Liệu mô hình nhà nước có vi phạm Nguyên tắc thay thế Liskov?


17

Hình ảnh này được lấy từ Áp dụng các mẫu và thiết kế hướng tên miền: Với các ví dụ trong C # và .NET

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

Đây là sơ đồ lớp cho MẫuSalesOrder trạng thái nơi a có thể có các trạng thái khác nhau trong suốt thời gian tồn tại của nó. Chỉ có sự chuyển tiếp nhất định được cho phép giữa các trạng thái khác nhau.

Bây giờ OrderStatelớp là một abstractlớp và tất cả các phương thức của nó được kế thừa vào các lớp con của nó. Nếu chúng ta xem xét lớp con Cancelledlà trạng thái cuối cùng không cho phép chuyển đổi sang bất kỳ trạng thái nào khác, chúng ta sẽ phải sử dụng overridetất cả các phương thức của nó trong lớp này để đưa ra ngoại lệ.

Bây giờ không vi phạm nguyên tắc thay thế của Liskov vì một sublcass không nên thay đổi hành vi của cha mẹ? Việc thay đổi lớp trừu tượng thành một giao diện có sửa lỗi này không?
Làm thế nào để sửa cái này?


Thay đổi OrderState thành một lớp cụ thể đưa ra các ngoại lệ "Không được hỗ trợ" trong tất cả các phương thức của nó theo mặc định?
James

Tôi chưa đọc cuốn sách này, nhưng có vẻ kỳ lạ với tôi rằng OrderState chứa Hủy () và sau đó có trạng thái Hủy, đó là kiểu phụ khác.
Euphoric

@Euphoric tại sao nó lạ? gọi cancel()thay đổi trạng thái để hủy bỏ.
Songo

Làm sao? Làm thế nào bạn có thể gọi Hủy trên OrderState, khi thể hiện trạng thái trong SalesOrder được thay đổi. Tôi nghĩ rằng toàn bộ mô hình là sai. Tôi muốn xem thực hiện đầy đủ.
Euphoric

Câu trả lời:


3

Điều này đặc biệt thực hiện, vâng. Nếu bạn thực hiện các lớp cụ thể của tiểu bang chứ không phải là người triển khai trừu tượng thì bạn sẽ thoát khỏi điều này.

Tuy nhiên, mẫu trạng thái mà bạn đang đề cập đến một cách hiệu quả là thiết kế máy trạng thái nói chung là thứ tôi không đồng ý với cách tôi đã thấy nó phát triển. Tôi nghĩ rằng có đủ cơ sở để giải mã điều này là vi phạm nguyên tắc trách nhiệm duy nhất bởi vì các mô hình quản lý nhà nước này cuối cùng trở thành kho lưu trữ trung tâm cho kiến ​​thức về tình trạng hiện tại của nhiều bộ phận khác trong hệ thống. Bộ phận quản lý nhà nước này được tập trung hóa thường xuyên hơn sẽ không yêu cầu các quy tắc kinh doanh liên quan đến nhiều bộ phận khác nhau của hệ thống để phối hợp chúng một cách hợp lý.

Hãy tưởng tượng nếu tất cả các bộ phận của hệ thống quan tâm đến trạng thái ở các dịch vụ khác nhau, các quy trình khác nhau trên các máy khác nhau, trình quản lý trạng thái trung tâm sẽ nêu chi tiết trạng thái cho từng vị trí đó làm tắc nghẽn toàn bộ hệ thống phân tán này và tôi nghĩ rằng tắc nghẽn là một dấu hiệu vi phạm SRP cũng như thiết kế nói chung là xấu.

Ngược lại, tôi sẽ đề nghị một đối tượng làm cho các đối tượng thông minh hơn như trong đối tượng Mô hình trong mô hình MVC nơi mô hình biết cách tự xử lý, nó không cần một người điều phối bên ngoài để quản lý hoạt động bên trong của nó hoặc lý do cho nó.

Ngay cả việc đặt một mẫu trạng thái như thế này bên trong một đối tượng để nó chỉ tự quản lý, có cảm giác như bạn sẽ làm cho đối tượng đó quá lớn. Quy trình công việc nên được thực hiện thông qua thành phần của các đối tượng tự chịu trách nhiệm khác nhau, thay vì với một trạng thái được phối hợp duy nhất quản lý dòng chảy của các đối tượng khác hoặc dòng chảy của trí thông minh trong chính nó.

Nhưng tại thời điểm đó, nó nghệ thuật hơn kỹ thuật và do đó, chắc chắn chủ quan của bạn đối với một số trong những điều đó, nói rằng các nguyên tắc là một hướng dẫn tốt và có, việc thực hiện mà bạn liệt kê vi phạm LSP, nhưng có thể không được sửa. Chỉ cần rất cẩn thận về SRP khi sử dụng bất kỳ mẫu nào có tính chất này và bạn có thể an toàn.


Về cơ bản, vấn đề lớn là định hướng đối tượng là tốt cho việc thêm các lớp mới nhưng làm cho việc thêm các phương thức mới trở nên khó khăn. Và trong trường hợp của trạng thái, như bạn đã nói, không có khả năng là bạn sẽ cần phải mở rộng mã với các trạng thái mới rất thường xuyên.
hugomg

3
@Jimmy Hoffa Xin lỗi, nhưng ý bạn chính xác là gì bởi "Nếu bạn tạo ra các lớp cụ thể chứ không phải là người triển khai trừu tượng thì bạn sẽ thoát khỏi điều này." ?
Songo

@Jimmy: Tôi đã thử triển khai trạng thái mà không có mẫu trạng thái bằng cách mỗi thành phần chỉ biết về trạng thái của chính nó. Điều này đã kết thúc việc thực hiện các thay đổi lớn để thực hiện và duy trì phức tạp hơn đáng kể. Điều đó nói rằng, tôi nghĩ rằng việc sử dụng một máy trạng thái (lý tưởng là thư viện của người khác để buộc nó được coi là một hộp đen) chứ không phải là mẫu thiết kế trạng thái (do đó, các trạng thái chỉ là các phần tử trong một enum chứ không phải là các lớp đầy đủ và chuyển tiếp chỉ là câm các cạnh) mang lại nhiều lợi ích của mẫu trạng thái trong khi thù địch với các nỗ lực của các nhà phát triển lạm dụng nó.
Brian

8

một sublcass không nên thay đổi hành vi của cha mẹ?

Đó là cách hiểu sai về LSP. Một lớp con có thể thay đổi hành vi của cha mẹ, miễn là nó vẫn đúng với kiểu cha mẹ.

Có một lời giải thích dài trên Wikipedia , cho thấy những điều sẽ vi phạm LSP:

... có một số điều kiện hành vi mà kiểu phụ phải đáp ứng. Đây là những chi tiết trong một thuật ngữ giống như thiết kế theo phương pháp hợp đồng, dẫn đến một số hạn chế về cách hợp đồng có thể tương tác với kế thừa:

  • Điều kiện tiên quyết không thể được tăng cường trong một kiểu con.
  • Postconditions không thể được làm yếu trong một kiểu con.
  • Bất biến của siêu kiểu phải được bảo toàn trong một kiểu con.
  • Ràng buộc lịch sử ("quy tắc lịch sử"). Các đối tượng được coi là có thể sửa đổi chỉ thông qua các phương thức của chúng (đóng gói). Vì các kiểu con có thể giới thiệu các phương thức không có trong siêu kiểu, nên việc giới thiệu các phương thức này có thể cho phép thay đổi trạng thái trong kiểu con không được phép trong siêu kiểu. Các ràng buộc lịch sử cấm điều này. Đó là yếu tố tiểu thuyết được giới thiệu bởi Liskov và Wing. Một hành vi vi phạm ràng buộc này có thể được minh họa bằng cách định nghĩa MutablePoint là một kiểu con của ImmutablePoint. Đây là vi phạm ràng buộc lịch sử, bởi vì trong lịch sử của Điểm bất biến, trạng thái luôn giống nhau sau khi tạo, do đó, nó không thể bao gồm lịch sử của MutablePoint nói chung. Tuy nhiên, các trường được thêm vào kiểu con có thể được sửa đổi một cách an toàn vì chúng không thể quan sát được thông qua các phương thức siêu kiểu.

Cá nhân, tôi thấy dễ dàng hơn để nhớ điều này: Nếu tôi đang xem một tham số trong một phương thức có loại A, liệu ai đó có vượt qua một kiểu con B có gây cho tôi bất ngờ không? Nếu họ sau đó sẽ vi phạm LSP.

Là ném một ngoại lệ là một bất ngờ? Không hẳn vậy. Đó là điều có thể xảy ra bất cứ lúc nào, cho dù tôi đang gọi phương thức Ship trên OrderState hay Granted hoặc Shipped. Vì vậy, tôi phải tính đến nó và nó không thực sự vi phạm LSP.

Điều đó nói rằng , tôi nghĩ có nhiều cách tốt hơn để xử lý tình huống này. Nếu tôi viết điều này bằng C #, tôi sẽ sử dụng các giao diện và kiểm tra việc thực hiện giao diện trước khi gọi phương thức. Ví dụ: nếu OrderState hiện tại không triển khai IShippable, đừng gọi phương thức Ship trên đó.

Nhưng sau đó tôi cũng sẽ không sử dụng mô hình Bang cho tình huống cụ thể này. Mẫu Trạng thái phù hợp hơn nhiều với trạng thái của ứng dụng so với trạng thái của một đối tượng miền như thế này.

Vì vậy, một cách ngắn gọn, đây là một ví dụ kém về Mô hình Nhà nước và không phải là một cách đặc biệt tốt để xử lý trạng thái của một đơn đặt hàng. Nhưng nó được cho là không vi phạm LSP. Và mô hình Nhà nước, tự bản thân nó, chắc chắn là không.


Nghe có vẻ như bạn đang nhầm lẫn LSP với Nguyên tắc ít ngạc nhiên / ngạc nhiên nhất, tôi tin rằng LSP có ranh giới rõ ràng hơn khi điều này vi phạm chúng, mặc dù bạn đang áp dụng Pola chủ quan hơn dựa trên những kỳ vọng chủ quan ; đó là mỗi người mong đợi một cái gì đó khác nhau hơn LSP dựa trên các bảo đảm theo hợp đồng và được xác định rõ ràng do nhà cung cấp đưa ra, không được đánh giá chủ quan về những kỳ vọng mà người tiêu dùng đoán được.
Jimmy Hoffa

@JimmyHoffa: Bạn nói đúng, và đó là lý do tại sao tôi nói ra ý nghĩa chính xác trước khi tôi nêu một cách đơn giản hơn để ghi nhớ LSP. Tuy nhiên, nếu có một câu hỏi trong đầu tôi "bất ngờ" nghĩa là gì thì tôi sẽ quay lại và kiểm tra các quy tắc chính xác của LSP (bạn có thể thực sự nhớ lại chúng theo ý muốn không?). Các ngoại lệ thay thế chức năng (hoặc ngược lại) là một điều khó khăn vì nó không vi phạm bất kỳ điều khoản cụ thể nào ở trên, đây có thể là một đầu mối mà nó nên được xử lý theo một cách hoàn toàn khác.
pdr

1
Một ngoại lệ là một điều kiện dự kiến ​​được xác định trong hợp đồng trong suy nghĩ của tôi, nghĩ về NetworkSocket, nó có thể có một ngoại lệ dự kiến ​​khi gửi nếu nó không mở, nếu việc triển khai của bạn không ném ngoại lệ đó cho một ổ cắm kín bạn đang vi phạm LSP , nếu hợp đồng nêu rõ điều ngược lại và tiểu loại của bạn không ném ngoại lệ thì bạn đang vi phạm LSP. Đó ít nhiều là cách tôi phân loại các ngoại lệ trong LSP. Như để nhớ các quy tắc; Tôi có thể sai về điều này và cảm thấy thoải mái khi nói với tôi như vậy, nhưng sự hiểu biết của tôi về LSP vs Pola được xác định trong câu cuối cùng của nhận xét của tôi ở trên.
Jimmy Hoffa

1
@JimmyHoffa: Tôi chưa bao giờ coi Pola và LSP thậm chí được kết nối từ xa (tôi nghĩ về Pola theo thuật ngữ UI), nhưng bây giờ bạn đã chỉ ra rằng, chúng là như vậy. Tôi không chắc chắn rằng họ đang xung đột, hoặc người này chủ quan hơn người kia. Không phải LSP là một mô tả sớm về Pola trong các thuật ngữ kỹ thuật phần mềm? Theo cùng một cách mà tôi cảm thấy thất vọng khi Ctrl-C không phải là Sao chép (Pola), tôi cũng cảm thấy thất vọng khi một ImmutablePoint có thể thay đổi được vì nó thực sự là một lớp con MutablePoint (LSP).
pdr

1
Tôi sẽ coi đó CircleWithFixedCenterButMutableRadiuslà vi phạm LSP có thể xảy ra nếu hợp đồng tiêu dùng ImmutablePointquy định rằng nếu X.equals(Y), người tiêu dùng có thể tự do thay thế X cho Y và ngược lại, và do đó phải hạn chế sử dụng lớp theo cách thay thế như vậy sẽ gây rắc rối. Loại có thể có thể xác định một cách hợp pháp equalssao cho tất cả các trường hợp sẽ so sánh là khác biệt, nhưng hành vi đó có thể sẽ hạn chế tính hữu dụng của nó.
supercat

2

(Điều này được viết từ quan điểm của C #, vì vậy không có trường hợp ngoại lệ nào được kiểm tra.)

Theo bài viết trên Wikipedia về LSP , một trong những điều kiện của LSP là:

Không có ngoại lệ mới nào được đưa ra bằng các phương thức của kiểu con, ngoại trừ trường hợp các ngoại lệ đó là các kiểu con của ngoại lệ được ném bởi các phương thức của siêu kiểu.

Làm thế nào bạn nên hiểu điều đó? Chính xác thì những trường hợp ngoại lệ nào được ném bởi các phương thức của supertype khi các phương thức của siêu kiểu là trừu tượng? Tôi nghĩ đó là những ngoại lệ được ghi nhận là những ngoại lệ có thể được ném ra bởi các phương pháp của siêu kiểu.

Điều này có nghĩa là nếu OrderState.Ship()được ghi lại như một thứ gì đó giống như ném ra InvalidOperationExceptionnếu hoạt động này không được hỗ trợ bởi trạng thái hiện tại, thì tôi nghĩ rằng thiết kế này không phá vỡ LSP. Mặt khác, nếu các phương thức siêu kiểu không được ghi lại theo cách này, LSP bị vi phạm.

Nhưng điều này không có nghĩa đây là thiết kế tốt, bạn không nên sử dụng ngoại lệ cho luồng điều khiển thông thường, điều này có vẻ rất gần với. Ngoài ra, trong một ứng dụng thực tế, có lẽ bạn sẽ muốn biết liệu một thao tác có thể được thực hiện hay không trước khi bạn cố gắng thực hiện, ví dụ như để vô hiệu hóa nút Nút Ship Ship trong giao diện người dùng.


Trên thực tế đây là điểm chính xác mà gửi cho tôi suy nghĩ. Nếu các lớp con ném ngoại lệ không được xác định trong cha thì nó phải là vi phạm LSP.
Songo

Tôi nghĩ rằng trong một ngôn ngữ mà các trường hợp ngoại lệ không được kiểm tra, ném chúng không phải là vi phạm LSP. Đó chỉ là một khái niệm về ngôn ngữ, rằng bất cứ nơi nào bất cứ lúc nào cũng có thể ném ngoại lệ. Vì vậy, bất kỳ mã khách hàng nên sẵn sàng cho điều đó.
Zapadlo
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.