Nguyên tắc thay thế Liskov không tương thích với Introspection hay Duck Typing?


11

Tôi có hiểu chính xác rằng Nguyên tắc thay thế Liskov không thể được quan sát bằng các ngôn ngữ nơi các đối tượng có thể tự kiểm tra, giống như những gì thường thấy trong các ngôn ngữ gõ vịt?

Ví dụ, trong Ruby, nếu một lớp Bkế thừa từ một lớp A, thì với mọi đối tượng xcủa A, x.classsẽ quay trở lại A, nhưng nếu xlà một đối tượng của B, x.classsẽ không quay trở lại A.

Đây là một tuyên bố của LSP:

Hãy q (x) là một chứng minh tài sản về đối tượng x kiểu T . Sau đó q (y) nên chứng minh cho các đối tượng y kiểu S nơi S là một subtype của T .

Vì vậy, trong Ruby, ví dụ,

class T; end
class S < T; end

vi phạm LSP dưới hình thức này, như được chứng kiến ​​bởi tài sản q (x) =x.class.name == 'T'


Thêm vào. Nếu câu trả lời là "có" (LSP không tương thích với hướng nội), thì câu hỏi khác của tôi sẽ là: có một dạng LSP "yếu" được sửa đổi nào có thể giữ cho ngôn ngữ động, có thể trong một số điều kiện bổ sung và chỉ với các loại đặc biệt của tài sản .


Cập nhật. Để tham khảo, đây là một công thức LSP khác mà tôi đã tìm thấy trên web:

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ó.

Và một cái khác:

Nếu S là một kiểu con được khai báo của T, các đối tượng thuộc loại S sẽ hành xử như các đối tượng của loại T được dự kiến ​​sẽ hành xử, nếu chúng được coi là các đối tượng của loại T.

Cái cuối cùng được chú thích với:

Lưu ý rằng LSP là tất cả về hành vi dự kiến ​​của các đối tượng. Người ta chỉ có thể theo LSP nếu người ta biết rõ hành vi dự kiến ​​của các đối tượng là gì.

Điều này dường như yếu hơn so với bản gốc và có thể quan sát được, nhưng tôi muốn thấy nó được chính thức hóa, đặc biệt giải thích ai là người quyết định hành vi dự kiến ​​là gì.

Có phải LSP không phải là một thuộc tính của một cặp lớp trong ngôn ngữ lập trình, mà là một cặp lớp cùng với một tập các thuộc tính nhất định, được thỏa mãn bởi lớp tổ tiên? Trên thực tế, điều này có nghĩa là để xây dựng một lớp con (lớp con cháu) tôn trọng LSP, tất cả các cách sử dụng có thể có của lớp tổ tiên phải được biết đến? Theo LSP, lớp tổ tiên được cho là có thể thay thế với bất kỳ lớp con cháu nào, phải không?


Cập nhật. Tôi đã chấp nhận câu trả lời, nhưng tôi muốn thêm một ví dụ cụ thể từ Ruby để minh họa cho câu hỏi. Trong Ruby, mỗi lớp là một mô-đun theo nghĩa là Classlớp là hậu duệ của Modulelớp. Tuy nhiên:

class C; end
C.is_a?(Module) # => true
C.class # => Class
Class.superclass # => Module

module M; end
M.class # => Module

o = Object.new

o.extend(M) # ok
o.extend(C) # => TypeError: wrong argument type Class (expected Module)

2
Hầu như tất cả các ngôn ngữ hiện đại đều cung cấp một mức độ nội tâm nào đó, vì vậy câu hỏi không thực sự cụ thể đối với Ruby.
Joachim Sauer

Tôi hiểu, tôi đã cho Ruby làm ví dụ. Tôi không biết, có thể trong một số ngôn ngữ khác có nội tâm có một số "dạng yếu" của LSP, nhưng, nếu tôi hiểu đúng nguyên tắc, nó không tương thích với hướng nội.
Alexey

Tôi đã xóa "Ruby" khỏi tiêu đề.
Alexey

2
Câu trả lời ngắn gọn là chúng tương thích. Đây là một bài đăng trên blog mà tôi hầu hết đồng ý với: Nguyên tắc thay thế Liskov cho việc gõ vịt
K.Steff

2
@Alexey Thuộc tính trong ngữ cảnh này là bất biến của một đối tượng. Ví dụ, các đối tượng bất biến có thuộc tính mà giá trị của chúng không thay đổi. Nếu bạn nhìn vào các bài kiểm tra đơn vị tốt, họ nên kiểm tra chính xác các thuộc tính này.
K.Steff

Câu trả lời:


29

Đây là nguyên tắc thực tế :

Hãy q(x)là một tài sản chứng minh về các đối tượng xcủa loại T. Sau đó q(y)nên chứng minh cho các đối tượng ycủa loại hình Snơi Slà một subtype của T.

Và tóm tắt wikipedia tuyệt vời :

Nó 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 (nghĩa là các đối tượng thuộc loại S có thể được thay thế cho các đối tượng loại T) mà không thay đổi bất kỳ các thuộc tính mong muốn của chương trình đó (tính chính xác, nhiệm vụ được thực hiện, v.v.).

Và một số trích dẫn có liên quan từ bài báo:

Điều cần thiết là một yêu cầu mạnh mẽ hơn, hạn chế hành vi của các loại phụ: các thuộc tính có thể được chứng minh bằng cách sử dụng đặc tả của loại được cho là của đối tượng nên giữ ngay cả khi đối tượng thực sự là thành viên của một kiểu con ...

Một đặc tả loại bao gồm các thông tin sau:
- Tên của loại;
- Mô tả về không gian giá trị của loại;
- Đối với mỗi phương thức của loại:
--- Tên của nó;
--- Chữ ký của nó (bao gồm các trường hợp ngoại lệ được báo hiệu);
--- Hành vi của nó về các điều kiện trước và sau điều kiện.

Vì vậy, cho câu hỏi:

Tôi có hiểu chính xác rằng Nguyên tắc thay thế Liskov không thể được quan sát bằng các ngôn ngữ nơi các đối tượng có thể tự kiểm tra, giống như những gì thường thấy trong các ngôn ngữ gõ vịt?

Không.

A.classtrả về một lớp
B.classtrả về một lớp

Vì bạn có thể thực hiện cùng một cuộc gọi trên loại cụ thể hơn và nhận được kết quả tương thích, LSP giữ. Vấn đề là với các ngôn ngữ động, bạn vẫn có thể gọi mọi thứ về kết quả mong đợi chúng ở đó.

Nhưng hãy xem xét một ngôn ngữ gõ tĩnh, cấu trúc (vịt). Trong trường hợp này, A.classsẽ trả về một loại có ràng buộc rằng nó phải là Ahoặc một kiểu con của A. Điều này cung cấp đảm bảo tĩnh rằng bất kỳ kiểu con nào Acũng phải cung cấp một phương thức T.classcó kết quả là một kiểu thỏa mãn ràng buộc đó.

Điều này cung cấp một khẳng định mạnh mẽ hơn rằng LSP giữ các ngôn ngữ hỗ trợ gõ vịt và bất kỳ hành vi vi phạm LSP nào trong Ruby như xảy ra nhiều hơn do lạm dụng động thông thường so với không tương thích thiết kế ngôn ngữ.


1
"Vì bạn có thể thực hiện cùng một cuộc gọi trên loại cụ thể hơn và nhận được kết quả tương thích, LSP giữ". LSP giữ nếu kết quả là giống hệt nhau, nếu tôi hiểu đúng. Có lẽ có thể có một số dạng LSP "tuần" đối với các ràng buộc nhất định, yêu cầu thay vì tất cả các thuộc tính chỉ có các ràng buộc nhất định được thỏa mãn. Trong mọi trường hợp, tôi sẽ đánh giá cao bất kỳ tài liệu tham khảo.
Alexey

@Alexey được chỉnh sửa để bao gồm ý nghĩa của LSP. Nếu tôi có thể sử dụng B nơi tôi mong đợi A, thì LSP giữ. Tôi tò mò về cách bạn nghĩ. Lớp của Ruby có thể vi phạm điều đó.
Telastyn

3
@Alexey - Nếu chương trình của bạn chứa fail unless x.foo == 42và một kiểu con trả về 0 thì đó là điều tương tự. Đây không phải là thất bại của LSP, đó là hoạt động bình thường của chương trình của bạn. Đa hình không phải là vi phạm LSP.
Telastyn

1
@Alexey - Chắc chắn rồi. Hãy giả sử rằng đó là một tài sản. Trong trường hợp đó, mã của bạn không vi phạm LSP vì nó không cho phép các kiểu con có cùng hành vi ngữ nghĩa. Nhưng nó không đặc biệt đặc biệt đối với các ngôn ngữ gõ động hoặc vịt. Không có gì họ làm trong thiết kế ngôn ngữ của họ gây ra vi phạm. Mã bạn đã viết. Hãy nhớ rằng, LSP là một nguyên tắc thiết kế chương trình (do đó nên có trong định nghĩa) chứ không phải là một tính chất toán học của các chương trình.
Telastyn

6
@Alexey: nếu bạn viết một cái gì đó phụ thuộc vào x. Class == A, thì đó là mã của bạn vi phạm LSP , không phải ngôn ngữ. Có thể viết mã vi phạm LSP trong hầu hết mọi ngôn ngữ lập trình.
Andres F.

7

Trong ngữ cảnh của LSP, "thuộc tính" là thứ có thể được quan sát trên một loại (hoặc một đối tượng). Cụ thể, nó nói về một "tài sản có thể chứng minh".

Một "tài sản" như vậy có thể tồn tại một foo()phương thức không có giá trị trả về (và tuân theo hợp đồng được đặt trong tài liệu của nó).

Đảm bảo bạn không nhầm lẫn thuật ngữ này với "property" như trong " classlà một thuộc tính của mọi đối tượng trong Ruby". Một "tài sản" như vậy có thể là "tài sản LSP", nhưng nó không tự động giống nhau!

Bây giờ câu trả lời cho câu hỏi của bạn phụ thuộc rất nhiều vào cách bạn xác định "tài sản" nghiêm ngặt. Nếu bạn nói "tài sản của lớp A.classsẽ trả về kiểu của đối tượng", sau đó Bthực sự không có tài sản đó.

Tuy nhiên, nếu bạn xác định "tài sản" là " .classlợi nhuận A", sau đó rõ ràng là Bkhông không có tài sản đó.

Tuy nhiên, định nghĩa thứ hai không hữu ích lắm, vì về cơ bản bạn đã tìm thấy một cách xoay quanh để khai báo một hằng số.


Tôi chỉ có thể nghĩ về một định nghĩa về "thuộc tính" của chương trình: đối với đầu vào đã cho, nó trả về một giá trị đã cho hoặc nói chung, khi được sử dụng như một khối trong chương trình khác, chương trình khác cho đầu vào đã cho sẽ trả về các giá trị đã cho. Với định nghĩa này, tôi không thấy ý nghĩa của việc " .classsẽ trả về loại đối tượng". Nếu nó có nghĩa là x.class == x.class, đây không phải là một tài sản thú vị.
Alexey

1
@Alexey: Tôi đã cập nhật câu hỏi của mình với sự làm rõ về "tài sản" nghĩa là gì trong bối cảnh LSP.
Joachim Sauer

2
@Alexey: nhìn vào bài báo tôi không tìm thấy một định nghĩa cụ thể hoặc "tài sản". Điều đó có thể là do thuật ngữ này được sử dụng theo nghĩa chung của CS "một cái gì đó có thể được quan sát / chứng minh về một số đối tượng". Nó không có gì để làm với "một lĩnh vực của một đối tượng" khác.
Joachim Sauer

4
@Alexey: Tôi không biết tôi có thể nói gì thêm với bạn. Tôi sử dụng định nghĩa "một tài sản là một số chất lượng hoặc chứng nhận của một đối tượng". "màu" là một thuộc tính của vật thể, vật thể nhìn thấy được. "mật độ" là một tính chất của vật liệu. "có một phương thức được chỉ định" là một thuộc tính của một lớp / đối tượng.
Joachim Sauer

4
@Alexey: Tôi nghĩ rằng bạn đang ném em bé bằng nước tắm: Chỉ vì một số tính chất, LSP không thể được giữ để giữ không có nghĩa là nó vô dụng hoặc "không giữ được ngôn ngữ nào". Nhưng cuộc thảo luận đó sẽ đi xa đến đây.
Joachim Sauer

5

Theo tôi hiểu, không có gì về nội tâm không tương thích với LSP. Về cơ bản, miễn là một đối tượng hỗ trợ các phương thức giống như các phương thức khác, hai đối tượng sẽ có thể hoán đổi cho nhau. Nghĩa là, nếu mã của bạn hy vọng một Addressđối tượng, sau đó nó không quan trọng nếu đó là một CustomerAddresshoặc một WarehouseAddress, miễn là cả hai cung cấp (ví dụ) getStreetAddress(), getCityName(), getRegion()getPostalCode(). Bạn chắc chắn có thể tạo một số loại trang trí có một loại đối tượng khác và sử dụng hướng nội để cung cấp các phương thức cần thiết (ví dụ: một DestinationAddresslớp lấy một Shipmentđối tượng và trình bày địa chỉ tàu như một Address), nhưng nó không bắt buộc và chắc chắn sẽ không 'Ngăn chặn LSP được áp dụng.


2
@Alexey: Các đối tượng là "giống nhau" nếu chúng hỗ trợ các phương thức tương tự. Điều này có nghĩa là cùng tên, cùng số lượng và loại đối số, cùng loại trả về và cùng tác dụng phụ (miễn là chúng hiển thị với mã gọi). Các phương pháp có thể hành xử hoàn toàn khác nhau, nhưng miễn là họ tôn trọng hợp đồng, điều đó ổn.
TMN

1
@Alexey: nhưng tại sao tôi lại có một hợp đồng như vậy? Những gì sử dụng thực tế mà hợp đồng thực hiện? Nếu tôi có một hợp đồng như vậy, tôi chỉ có thể thay thế mọi trường x.class.namehợp bằng 'A' , thực sự x.class.name vô dụng .
Joachim Sauer

1
@Alexey: một lần nữa: chỉ vì bạn có thể xác định hợp đồng không thể hoàn thành bằng cách mở rộng một lớp khác không phá vỡ LSP. Nó chỉ có nghĩa là bạn xây dựng một lớp không thể mở rộng. Nếu tôi xác định phương thức "trả về nếu khối mã được cung cấp kết thúc trong thời gian hữu hạn " thì tôi cũng có một hợp đồng không thể thực hiện được. Điều đó không có nghĩa là lập trình là vô ích.
Joachim Sauer

2
@Alexey đang cố gắng xác định xem x.class.name == 'A'có phải là kiểu chống vịt trong việc gõ vịt hay không: sau tất cả, việc gõ vịt xuất phát từ "Nếu nó quẫy và đi như một con vịt, thì đó là một con vịt". Vì vậy, nếu nó hành xử như thế Avà tôn trọng các hợp đồng Ađưa ra, thì đó là một Aví dụ
K.Steff

1
@Alexey Bạn đã được trình bày với các định nghĩa rõ ràng. Lớp của một đối tượng không phải là một phần của hành vi hoặc hợp đồng của nó hoặc bất cứ điều gì bạn muốn gọi nó. Bạn đang tương đương "thuộc tính" với "trường đối tượng, chẳng hạn như x.myField", như đã được chỉ ra, nó KHÔNG giống nhau. Trong bối cảnh này, một thuộc tính giống như một thuộc tính toán học , giống như các bất biến kiểu. Hơn nữa, đây là kiểu chống mẫu để kiểm tra loại chính xác nếu bạn muốn gõ vịt. Vì vậy, vấn đề của bạn với LSP và gõ vịt, một lần nữa là gì? ;)
Andres F.

4

Sau khi xem qua bài viết gốc của Barbara Liskov, tôi đã tìm ra cách hoàn thành định nghĩa Wikipedia để LSP thực sự có thể hài lòng ở hầu hết mọi ngôn ngữ.

Trước hết, từ "có thể chứng minh" là quan trọng trong định nghĩa. Nó không được giải thích trong bài viết Wikipedia và "các ràng buộc" được đề cập ở nơi khác mà không cần tham khảo.

Đây là trích dẫn quan trọng đầu tiên từ bài báo:

Điều cần thiết là một yêu cầu mạnh mẽ hơn, hạn chế hành vi của các loại phụ: các thuộc tính có thể được chứng minh bằng cách sử dụng đặc tả của loại được cho là của đối tượng nên giữ ngay cả khi đối tượng thực sự là thành viên của một kiểu con ...

Và đây là cái thứ hai, giải thích một đặc tả kiểu là gì:

Một đặc điểm kỹ thuật loại bao gồm các thông tin sau:

  • Tên loại;
  • Một mô tả về không gian giá trị của loại;
  • Đối với mỗi phương thức của loại:
    • Tên của nó;
    • Chữ ký của nó (bao gồm các trường hợp ngoại lệ được báo hiệu);
    • Hành vi của nó về các điều kiện trước và sau điều kiện.

Vì vậy, LSP chỉ có ý nghĩa đối với các đặc tả loại đã cho và đối với một đặc tả loại thích hợp (ví dụ đối với loại trống), nó có thể được thỏa mãn trong bất kỳ ngôn ngữ nào.

Tôi coi câu trả lời của Telastyn là gần nhất với những gì tôi đang tìm kiếm bởi vì "các ràng buộc" đã được đề cập rõ ràng.


Telastyn, nếu bạn có thể thêm những trích dẫn này vào câu trả lời của mình, tôi muốn chấp nhận câu nói của bạn hơn là của tôi.
Alexey

2
đánh dấu không hoàn toàn giống nhau, và tôi đã thay đổi một chút về sự nhấn mạnh, nhưng các trích dẫn được bao gồm.
Telastyn

Xin lỗi, Joachim Sauer đã đề cập đến các thuộc tính "có thể chứng minh" mà bạn không thích. Nói chung, bạn chỉ đơn thuần đưa ra các câu trả lời hiện có. Thành thật mà nói, tôi không biết bạn đang tìm kiếm điều gì ...
Andres F.

Không, nó không được giải thích "có thể chứng minh được từ cái gì?". Tài sản x.class.name = 'A'có thể chứng minh cho tất cả xcác lớp Anếu bạn cho phép quá nhiều kiến ​​thức. Đặc tả loại không được xác định và mối quan hệ chính xác của nó với LSP cũng không, mặc dù một số thông tin chính thức đã được đưa ra. Tôi đã tìm thấy những gì tôi đang tìm kiếm trong bài báo của Liskov và trả lời câu hỏi của tôi ở trên.
Alexey

Tôi nghĩ rằng những từ bạn nhấn mạnh là chìa khóa. Nếu các tài liệu siêu kiểu cho bất kỳ xloại nào, x.woozlesẽ mang lại undefined, thì không có loại x.woozlenào không mang lại undefinedsẽ là một kiểu con thích hợp. Nếu siêu kiểu này không ghi nhận bất cứ điều gì về x.woozlethực tế, thì việc sử dụng x.woozletrên siêu kiểu sẽ xảy ra mang lại undefinedsẽ không có nghĩa gì về nó, những gì có thể làm trên tiểu loại.
supercat

3

Để trích dẫn bài viết của Wikipedia về LSP , "tính thay thế là một nguyên tắc trong lập trình hướng đối tượng." Đó là một nguyên tắc và một phần của thiết kế chương trình của bạn. Nếu bạn viết mã phụ thuộc vào x.class == A, thì đó là mã của bạn đang vi phạm LSP. Lưu ý loại mã bị hỏng này cũng có thể có trong Java, không cần gõ vịt.

Không có gì trong gõ vịt vốn đã phá vỡ LSP. Chỉ khi bạn sử dụng sai nó, như trong ví dụ của bạn.

Suy nghĩ bổ sung: dù sao không kiểm tra rõ ràng cho một lớp đối tượng đánh bại mục đích gõ vịt, dù sao?


Bạn có thể đưa ra định nghĩa về LSP không?
Alexey

1
@Alexey Định nghĩa chính xác về LSP được nêu trong Wikipedia dưới dạng các kiểu con. Định nghĩa không chính thức là Liskov's notion of a behavioral subtype defines a notion of substitutability for mutable objects; that is, 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).. Lớp chính xác của một đối tượng KHÔNG phải là một trong "các thuộc tính mong muốn của chương trình"; nếu không, nó sẽ chạy ngược lại với không chỉ gõ vịt mà nói chung là phân loại, bao gồm cả hương vị của Java.
Andres F.

2
@Alexey Cũng lưu ý rằng ví dụ của bạn x.class == Avi phạm cả LSP gõ vịt. Không có ý nghĩa trong việc sử dụng gõ vịt nếu bạn sẽ kiểm tra các loại thực tế.
Andres F.

Andres, định nghĩa này không đủ chính xác để tôi hiểu. Chương trình nào, một cho trước, hay bất kỳ? Một tài sản mong muốn là gì? Nếu lớp nằm trong thư viện, các ứng dụng khác nhau có thể xem xét các thuộc tính khác nhau mong muốn. A không thấy dòng mã có thể vi phạm LSP như thế nào, vì tôi nghĩ rằng LSP là một thuộc tính của một cặp lớp trong một ngôn ngữ lập trình nhất định: hoặc ( A, B) có thỏa mãn LSP hay không. Nếu LSP phụ thuộc vào mã được sử dụng ở nơi khác, thì không được giải thích mã nào được phép. Tôi hy vọng sẽ tìm thấy một cái gì đó ở đây: cse.ohio-state.edu/~neelam/cifts/788/lwb.pdf
Alexey

2
@Alexey LSP giữ (hoặc không) cho một thiết kế cụ thể. Đó là một cái gì đó để tìm kiếm trong một thiết kế; nó không phải là một tài sản của ngôn ngữ nói chung. Nó không nhận được bất kỳ chính xác hơn so với định nghĩa thực tế : Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T. Rõ ràng đó x.classkhông phải là một trong những tính chất thú vị ở đây; mặt khác , tính đa hình của Java cũng không hoạt động. Không có gì cố hữu để gõ vịt trong "vấn đề x. Class" của bạn. Bạn có đồng ý cho đến nay?
Andres F.
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.