Các trường hợp đặc biệt với dự phòng có vi phạm Nguyên tắc thay thế Liskov không?


20

Giả sử tôi có một giao diện FooInterfacecó chữ ký sau:

interface FooInterface {
    public function doSomething(SomethingInterface something);
}

Và một lớp cụ thể ConcreteFoothực hiện giao diện đó:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
    }

}

Tôi muốn ConcreteFoo::doSomething()làm một cái gì đó độc đáo nếu nó vượt qua một loại SomethingInterfaceđối tượng đặc biệt (giả sử nó được gọi là SpecialSomething).

Đó chắc chắn là vi phạm LSP nếu tôi củng cố các điều kiện tiên quyết của phương pháp hoặc đưa ra một ngoại lệ mới, nhưng nó vẫn là vi phạm LSP nếu tôi sử SpecialSomethingdụng các đối tượng đặc biệt trong khi cung cấp dự phòng cho SomethingInterfacecác đối tượng chung ? Cái gì đó như:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
        if (something instanceof SpecialSomething) {
            // Do SpecialSomething magic
        }
        else {
            // Do generic SomethingInterface magic
        }
    }

}

Câu trả lời:


19

có thể là một sự vi phạm LSP tùy thuộc vào những gì khác trong hợp đồng cho doSomethingphương thức này. Nhưng nó gần như chắc chắn là mùi mã ngay cả khi nó không vi phạm LSP.

Ví dụ, nếu một phần của hợp đồng doSomethinglà nó sẽ gọi something.commitUpdates()ít nhất một lần trước khi quay lại và trong trường hợp đặc biệt mà nó gọi commitSpecialUpdates()thay thế, thì đó vi phạm LSP. Ngay cả khi phương pháp SpecialSomethingcủa nó commitSpecialUpdates()được thiết kế một cách có ý thức để thực hiện tất cả những thứ tương tự như commitUpdates(), đó chỉ là hack một cách có hiệu quả xung quanh vi phạm LSP, và chính xác là loại tin tặc sẽ không phải làm nếu một người tuân theo LSP một cách nhất quán. Cho dù bất cứ điều gì như thế này áp dụng cho trường hợp của bạn là điều gì đó bạn sẽ phải tìm ra bằng cách kiểm tra hợp đồng của bạn cho phương pháp đó (cho dù rõ ràng hay ngầm).

Lý do đây là mùi mã là vì việc kiểm tra loại cụ thể của một trong các đối số của bạn bỏ lỡ điểm xác định loại giao diện / trừu tượng cho nó ở vị trí đầu tiên và vì về nguyên tắc, bạn không còn có thể đảm bảo phương thức hoạt động được nữa (hãy tưởng tượng nếu ai đó viết một lớp con SpecialSomethingvới giả định commitUpdates()sẽ được gọi). Trước hết, hãy cố gắng làm cho các cập nhật đặc biệt này hoạt động trong phạm vi hiện cóSomethingInterface; đó là kết quả tốt nhất có thể Nếu bạn thực sự chắc chắn rằng bạn không thể làm điều đó, thì bạn cần cập nhật giao diện. Nếu bạn không kiểm soát giao diện, thì bạn có thể cần xem xét việc viết giao diện của riêng mình, thực hiện những gì bạn muốn. Nếu bạn thậm chí không thể đưa ra một giao diện phù hợp với tất cả chúng, có lẽ bạn nên loại bỏ hoàn toàn giao diện đó và có nhiều phương thức sử dụng các loại cụ thể khác nhau, hoặc có thể là một công cụ tái cấu trúc thậm chí còn lớn hơn. Chúng ta phải biết nhiều hơn về phép thuật mà bạn đã nhận xét để cho biết loại nào phù hợp.


Cảm ơn! Điều này có ích. Về mặt giả thuyết, mục đích của doSomething()phương thức sẽ là chuyển đổi kiểu thành SpecialSomething: nếu nhận được SpecialSomethingnó sẽ trả về đối tượng không bị thay đổi, trong khi nếu nhận được một SomethingInterfaceđối tượng chung, nó sẽ chạy thuật toán để chuyển đổi nó thành SpecialSomethingđối tượng. Vì các điều kiện tiên quyết và hậu điều kiện vẫn giữ nguyên, tôi không tin rằng hợp đồng đã bị vi phạm.
Evan

1
@Evan Oh wow ... đó là một trường hợp thú vị. Điều đó thực sự có thể là hoàn toàn không có vấn đề. Điều duy nhất tôi có thể nghĩ là nếu bạn trả lại đối tượng hiện tại thay vì xây dựng một đối tượng mới, có thể ai đó phụ thuộc vào phương thức này trả lại một đối tượng hoàn toàn mới ... nhưng liệu điều đó có thể phá vỡ mọi người hay không phụ thuộc vào ngôn ngữ. Có thể cho ai đó gọi y = doSomething(x), sau đó x.setFoo(3), và sau đó tìm thấy y.getFoo()trả về 3?
Ixrec

Đó là vấn đề tùy thuộc vào ngôn ngữ, mặc dù vậy nó nên được xử lý dễ dàng bằng cách chỉ trả lại một bản sao của SpecialSomethingđối tượng. Mặc dù vì mục đích thuần túy, tôi cũng có thể thấy việc từ bỏ tối ưu hóa trường hợp đặc biệt khi đối tượng được thông qua SpecialSomethingvà chỉ chạy nó thông qua thuật toán chuyển đổi lớn hơn vì nó vẫn hoạt động trên tài khoản của nó cũng là SomethingInterfaceđối tượng.
Evan

1

Đó không phải là vi phạm LSP. Tuy nhiên, nó vẫn vi phạm quy tắc "không thực hiện kiểm tra loại". Sẽ là tốt hơn để thiết kế mã theo cách mà đặc biệt phát sinh tự nhiên; có thể SomethingInterfacecần một thành viên khác có thể thực hiện điều này, hoặc có thể bạn cần tiêm một nhà máy trừu tượng ở đâu đó.

Tuy nhiên, đây không phải là một quy tắc khó và nhanh, vì vậy bạn cần quyết định xem sự đánh đổi đó có xứng đáng hay không. Ngay bây giờ bạn có một mùi mã, và một trở ngại có thể cho các cải tiến trong tương lai. Loại bỏ nó có thể có nghĩa là một kiến ​​trúc phức tạp hơn đáng kể. Không có thêm thông tin tôi không thể biết cái nào tốt hơn.


1

Không, lợi dụng thực tế là một đối số đã cho không chỉ cung cấp giao diện A, mà cả A2, không vi phạm LSP.

Chỉ cần đảm bảo rằng đường dẫn đặc biệt không có bất kỳ điều kiện trước mạnh hơn (ngoài những điều kiện được thử nghiệm khi quyết định thực hiện), cũng không có bất kỳ điều kiện hậu yếu hơn.

Các mẫu C ++ thường làm như vậy để cung cấp hiệu suất tốt hơn, ví dụ bằng cách yêu cầu InputIterators, nhưng đưa ra các đảm bảo bổ sung nếu được gọi bằng RandomAccessIterators.

Nếu bạn phải quyết định trong thời gian chạy thay thế (ví dụ: sử dụng truyền động), hãy cẩn thận với quyết định sẽ sử dụng tất cả các lợi ích tiềm năng của bạn hoặc thậm chí nhiều hơn.

Lợi dụng trường hợp đặc biệt thường đi ngược lại DRY (đừng lặp lại chính mình), vì bạn có thể phải sao chép mã và chống lại KISS (Giữ cho nó đơn giản), vì nó phức tạp hơn.


0

Có một sự đánh đổi giữa nguyên tắc "không kiểm tra kiểu" và "tách biệt các giao diện của bạn". Nếu nhiều lớp cung cấp một phương tiện khả thi nhưng có thể không hiệu quả để thực hiện một số nhiệm vụ và một vài trong số đó cũng có thể cung cấp một phương tiện tốt hơn và cần có mã có thể chấp nhận bất kỳ danh mục nào rộng hơn có thể thực hiện nhiệm vụ (có lẽ không hiệu quả) nhưng sau đó thực hiện nhiệm vụ một cách hiệu quả nhất có thể, cần phải có tất cả các đối tượng thực hiện một giao diện bao gồm một thành viên để nói liệu phương thức hiệu quả hơn có được hỗ trợ hay không và nếu không thì sẽ sử dụng phương thức hiệu quả hơn. có mã nhận đối tượng kiểm tra xem nó có hỗ trợ giao diện mở rộng không và bỏ mã nếu có.

Cá nhân, tôi ủng hộ cách tiếp cận trước đây, mặc dù tôi muốn các khung công tác hướng đối tượng như .NET sẽ cho phép các giao diện chỉ định các phương thức mặc định (làm cho các giao diện lớn hơn bớt đau đớn hơn khi làm việc). Nếu giao diện chung bao gồm các phương thức tùy chọn, thì một lớp trình bao bọc duy nhất có thể xử lý các đối tượng với nhiều kết hợp khả năng khác nhau trong khi hứa hẹn cho người tiêu dùng chỉ những khả năng đó có trong đối tượng được bao bọc ban đầu. Nếu nhiều chức năng được phân chia thành các giao diện khác nhau, thì sẽ cần một đối tượng trình bao bọc khác nhau cho mọi kết hợp giao diện khác nhau mà các đối tượng được bao bọc có thể cần hỗ trợ.


0

Nguyên tắc thay thế Liskov là về các kiểu con hoạt động theo một hợp đồng của siêu kiểu của họ. Vì vậy, như Ixrec đã viết, không có đủ thông tin để trả lời liệu đó có phải là vi phạm LSP hay không.

Những gì bị vi phạm ở đây mặc dù là một nguyên tắc đóng mở. Nếu bạn có một yêu cầu mới - Thực hiện phép thuật SpecialS Something - và bạn phải sửa đổi mã hiện có, thì bạn chắc chắn vi phạm OCP .

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.