Đây có phải là vi phạm Nguyên tắc thay thế Liskov không?


132

Giả sử chúng tôi có một danh sách các thực thể Nhiệm vụ và một ProjectTaskloại phụ. Nhiệm vụ có thể được đóng bất cứ lúc nào, ngoại trừ ProjectTaskskhông thể đóng khi chúng có trạng thái Bắt đầu. Giao diện người dùng phải đảm bảo tùy chọn đóng bắt đầu ProjectTaskkhông bao giờ có sẵn, nhưng một số biện pháp bảo vệ có trong miền:

public class Task
{
     public Status Status { get; set; }

     public virtual void Close()
     {
         Status = Status.Closed;
     }
}

public class ProjectTask : Task
{
     public override void Close()
     {
          if (Status == Status.Started) 
              throw new Exception("Cannot close a started Project Task");

          base.Close();
     }
}

Bây giờ khi gọi Close()vào Nhiệm vụ, có khả năng cuộc gọi sẽ thất bại nếu đó là ProjectTasktrạng thái bắt đầu, khi đó sẽ không thực hiện nếu đó là Nhiệm vụ cơ bản. Nhưng đây là yêu cầu kinh doanh. Nó sẽ thất bại. Điều này có thể được coi là vi phạm nguyên tắc thay thế Liskov không?


14
Hoàn hảo cho một ví dụ T vi phạm thay thế liskov. Không sử dụng thừa kế ở đây, và bạn sẽ ổn thôi.
Jimmy Hoffa

8
Bạn có thể muốn thay đổi nó thành : public Status Status { get; private set; }; nếu không Close()phương pháp có thể được làm việc xung quanh.
Công việc

5
Có lẽ đây chỉ là ví dụ này, nhưng tôi thấy không có lợi ích vật chất nào khi tuân thủ LSP. Đối với tôi, giải pháp này trong câu hỏi rõ ràng hơn, dễ hiểu hơn và dễ bảo trì hơn so với giải pháp tuân thủ LSP.
Ben Lee

2
@BenLee Không dễ bảo trì hơn. Nó chỉ trông như vậy bởi vì bạn đang nhìn thấy điều này trong sự cô lập. Khi hệ thống lớn, việc đảm bảo các kiểu Taskcon không đưa ra sự không tương thích kỳ quái trong mã đa hình mà chỉ biết Tasklà một vấn đề lớn. LSP không phải là một ý thích, nhưng được giới thiệu chính xác để giúp duy trì trong các hệ thống lớn.
Andres F.

8
@BenLee Hãy tưởng tượng bạn có một TaskCloserquy trình closesAllTasks(tasks). Quá trình này rõ ràng không cố gắng để bắt ngoại lệ; Rốt cuộc, nó không phải là một phần của hợp đồng rõ ràng Task.Close(). Bây giờ bạn giới thiệu ProjectTaskvà đột nhiên bạn TaskCloserbắt đầu ném ngoại lệ (có thể chưa được xử lý). Đây là một vấn đề lớn!
Andres F.

Câu trả lời:


173

Có, đó là vi phạm LSP. Nguyên tắc thay thế Liskov yêu cầu rằng

  • Đ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ể bị suy 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.

Ví dụ của bạn phá vỡ yêu cầu đầu tiên bằng cách củng cố điều kiện tiên quyết để gọi Close()phương thức.

Bạn có thể sửa nó bằng cách đưa điều kiện trước được tăng cường lên cấp cao nhất của hệ thống phân cấp thừa kế:

public class Task {
    public Status Status { get; set; }
    public virtual bool CanClose() {
        return true;
    }
    public virtual void Close() {
        Status = Status.Closed;
    }
}

Bằng cách quy định rằng một cuộc gọi Close()chỉ có hiệu lực ở trạng thái khi CanClose()trả về truebạn thực hiện điều kiện trước áp dụng cho Taskcũng như đối với việc ProjectTasksửa lỗi vi phạm LSP:

public class ProjectTask : Task {
    public override bool CanClose() {
        return Status != Status.Started;
    }
    public override void Close() {
        if (Status == Status.Started) 
            throw new Exception("Cannot close a started Project Task");
        base.Close();
    }
}

17
Tôi không thích sự trùng lặp của tấm séc đó. Tôi thích ném ngoại lệ vào Nhiệm vụ. Đóng và xóa ảo khỏi Đóng.
Euphoric

4
@Euphoric Điều đó là đúng, việc Closekiểm tra cấp cao nhất và thêm một bảo vệ DoClosesẽ là một sự thay thế hợp lệ. Tuy nhiên, tôi muốn ở gần nhất có thể với ví dụ của OP; cải thiện nó là một câu hỏi riêng biệt.
dasblinkenlight

5
@Euphoric: Nhưng bây giờ không có cách nào để trả lời câu hỏi, "Nhiệm vụ này có thể bị đóng không?" mà không cố gắng để đóng nó. Điều này không cần thiết buộc phải sử dụng các ngoại lệ để kiểm soát dòng chảy. Tôi sẽ thừa nhận, tuy nhiên, loại điều này có thể được đưa quá xa. Đưa ra quá xa, loại giải pháp này cuối cùng có thể mang lại một mớ hỗn độn. Bất kể, câu hỏi của OP gây ấn tượng với tôi nhiều hơn về các nguyên tắc, vì vậy câu trả lời của tháp ngà rất phù hợp. +1
Brian

30
@Brian CanClose vẫn còn đó. Nó vẫn có thể được gọi để kiểm tra nếu Nhiệm vụ có thể được đóng lại. Kiểm tra trong Đóng cũng nên gọi điều này.
Euphoric

5
@Euphoric: Ah, tôi hiểu lầm. Bạn nói đúng, điều đó làm cho một giải pháp sạch hơn nhiều.
Brian

82

Đúng. Điều này vi phạm LSP.

Đề nghị của tôi là thêm CanClosephương thức / thuộc tính vào tác vụ cơ sở, vì vậy bất kỳ tác vụ nào cũng có thể biết liệu tác vụ trong trạng thái này có thể bị đóng hay không. Nó cũng có thể cung cấp lý do tại sao. Và loại bỏ ảo từ Close.

Dựa trên nhận xét của tôi:

public class Task {
    public Status Status { get; private set; }

    public virtual bool CanClose(out String reason) {
        reason = null;
        return true;
    }
    public void Close() {
        String reason;
        if (!CanClose(out reason))
            throw new Exception(reason);

        Status = Status.Closed;
    }
}

public ProjectTask : Task {
    public override bool CanClose(out String reason) {
        if (Status != Status.Started)
        {
            reason = "Cannot close a started Project Task";
            return false;
        }
        return base.CanClose(out reason);
    }
}

3
Cảm ơn vì điều này, bạn đã lấy ví dụ của dasblinkenlight thêm một bước nữa, nhưng tôi đã thích lời giải thích của anh ấy. Xin lỗi tôi không thể chấp nhận 2 câu trả lời!
Paul T Davies

Tôi muốn biết lý do tại sao chữ ký là bool ảo công khai CanClose (lý do chuỗi ngoài) - bằng cách sử dụng bạn chỉ là bằng chứng trong tương lai? Hoặc có một cái gì đó tinh tế hơn mà tôi đang thiếu?
Reacher Gilt

3
@ReacherGilt Tôi nghĩ bạn nên kiểm tra xem / ref làm gì và đọc lại mã của tôi. Bạn bối rối. Đơn giản là "Nếu nhiệm vụ không thể đóng, tôi muốn biết tại sao."
Euphoric

2
không có sẵn trong tất cả các ngôn ngữ, trả lại một tuple (hoặc một đối tượng đơn giản gói gọn lý do và boolean sẽ giúp nó di chuyển tốt hơn trên các ngôn ngữ OO mặc dù phải trả giá bằng việc mất trực tiếp một bool. ủng hộ, không có gì sai với câu trả lời này
Newtopian

1
Và có ổn không khi tăng cường các điều kiện tiên quyết cho tài sản CanClose? Tức là thêm điều kiện?
John V

24

Nguyên tắc thay thế Liskov nói rằng một lớp cơ sở nên có thể thay thế bằng bất kỳ lớp con nào của anh ta mà không làm thay đổi bất kỳ thuộc tính mong muốn nào của chương trình. Vì chỉ ProjectTaskđưa ra một ngoại lệ khi đóng cửa, một chương trình sẽ phải được thay đổi thành hàng hóa cho điều đó, nên ProjectTaskđược sử dụng thay thế Task. Vì vậy, nó là một vi phạm.

Nhưng nếu bạn sửa đổi Tasktrong chữ ký của nó rằng nó có thể đưa ra một ngoại lệ khi đóng, thì bạn sẽ không vi phạm nguyên tắc.


Tôi sử dụng c # mà tôi không nghĩ có khả năng này, nhưng tôi biết Java có.
Paul T Davies

2
@PaulTDavies Bạn có thể trang trí một phương thức với những ngoại lệ mà nó ném ra, msdn.microsoft.com/en-us/l Library / 5ast78ax.aspx . Bạn nhận thấy điều này khi bạn di chuột qua một phương thức từ thư viện lớp cơ sở, bạn sẽ nhận được một danh sách các trường hợp ngoại lệ. Nó không được thực thi, nhưng nó làm cho người gọi nhận biết dù sao.
Despertar

18

Một vi phạm LSP yêu cầu ba bên. Loại T, Subtype S và chương trình P sử dụng T nhưng được cung cấp một thể hiện của S.

Câu hỏi của bạn đã cung cấp T (Nhiệm vụ) và S (ProjectTask), nhưng không phải P. Vì vậy, câu hỏi của bạn chưa đầy đủ và câu trả lời đủ điều kiện: Nếu tồn tại một P không mong đợi một ngoại lệ thì đối với P đó, bạn có LSP sự vi phạm. Nếu mọi P mong đợi một ngoại lệ thì không có vi phạm LSP.

Tuy nhiên, bạn làm có một SRP vi phạm. Thực tế là trạng thái của một nhiệm vụ có thể được thay đổi, và chính sách rằng một số nhiệm vụ ở một số trạng thái nhất định không nên được thay đổi sang các trạng thái khác, là hai trách nhiệm rất khác nhau.

  • Trách nhiệm 1: Thể hiện một nhiệm vụ.
  • Trách nhiệm 2: Thực hiện các chính sách thay đổi trạng thái của nhiệm vụ.

Hai trách nhiệm này thay đổi vì những lý do khác nhau và do đó phải ở trong các lớp riêng biệt. Nhiệm vụ nên xử lý thực tế là một nhiệm vụ và dữ liệu liên quan đến một nhiệm vụ. TaskStatePolicy nên xử lý cách chuyển đổi nhiệm vụ từ trạng thái này sang trạng thái khác trong một ứng dụng nhất định.


2
Trách nhiệm phụ thuộc rất nhiều vào miền và (trong ví dụ này) mức độ phức tạp của trạng thái nhiệm vụ và các thay đổi của nó. Trong trường hợp này, không có dấu hiệu nào cho thấy điều đó, vì vậy không có vấn đề gì với SRP. Đối với vi phạm LSP, tôi tin rằng tất cả chúng ta đều cho rằng người gọi không mong đợi một ngoại lệ và ứng dụng sẽ hiển thị thông báo hợp lý thay vì rơi vào trạng thái sai lầm.
Euphoric

Unca 'Bob trả lời? "Chúng tôi không xứng đáng! Chúng tôi không xứng đáng!". Dù sao ... Nếu mọi P mong đợi một ngoại lệ thì không có vi phạm LSP. NHƯNG nếu chúng ta quy định một thể hiện T không thể đưa ra một OpenTaskException(gợi ý, gợi ý) và mỗi P mong đợi một ngoại lệ thì điều đó nói gì về mã để giao diện, không thực hiện? Tôi đang nói về cái gì vậy? Tôi không biết. Tôi chỉ nói rằng tôi đang bình luận về câu trả lời Bob của Unca.
radarbob

3
Bạn đúng rằng việc chứng minh vi phạm LSP cần có ba đối tượng. Tuy nhiên, vi phạm LSP tồn tại nếu có BẤT K program chương trình P nào đúng trong trường hợp không có S nhưng không thành công với việc bổ sung S.
kevin cline

16

Điều này có thể hoặc không thể vi phạm LSP.

Nghiêm túc. Hãy nghe tôi nói.

Nếu bạn tuân theo LSP, các đối tượng thuộc loại ProjectTaskphải hành xử như các đối tượng thuộc loại Taskdự kiến ​​sẽ hành xử.

Vấn đề với mã của bạn là bạn chưa ghi lại cách các đối tượng thuộc loại Taskdự kiến ​​sẽ hành xử. Bạn đã viết mã, nhưng không có hợp đồng. Tôi sẽ thêm một hợp đồng cho Task.Close. Tùy thuộc vào hợp đồng tôi thêm, mã cho ProjectTask.Closehoặc không tuân theo LSP.

Đưa ra hợp đồng sau đây cho Nhiệm vụ. Đóng, mã cho ProjectTask.Close không tuân theo LSP:

     // Behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     // Default behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     public virtual void Close()
     {
         Status = Status.Closed;
     }

Đưa ra hợp đồng sau đây cho Nhiệm vụ. Đóng, mã cho ProjectTask.Close không tuân theo LSP:

     // Behaviour: Moves the task to the closed status if possible.
     // If this is not possible, this method throws an Exception
     // and leaves the status unchanged.
     // Default behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     public virtual void Close()
     {
         Status = Status.Closed;
     }

Các phương pháp có thể bị ghi đè phải được ghi lại theo hai cách:

  • "Hành vi" ghi lại những gì khách hàng có thể dựa vào, người biết đối tượng người nhận là một Task, nhưng không biết đó là trường hợp trực tiếp của lớp nào. Nó cũng cho các nhà thiết kế của các lớp con ghi đè là hợp lý và không hợp lý.

  • "Hành vi mặc định" ghi lại những gì khách hàng có thể dựa vào, người biết rằng đối tượng người nhận là một thể hiện trực tiếp của Task(tức là những gì bạn nhận được nếu bạn sử dụng new Task(). Nó cũng cho người thiết kế các lớp con biết hành vi nào sẽ được kế thừa nếu họ không ghi đè phương thức.

Bây giờ các quan hệ sau đây nên giữ:

  • Nếu S là một kiểu con của T, hành vi được ghi lại của S nên tinh chỉnh hành vi được ghi lại của T.
  • Nếu S là một kiểu con của (hoặc bằng) T, hành vi của mã S sẽ tinh chỉnh hành vi được ghi lại của T.
  • Nếu S là một kiểu con của (hoặc bằng) T, hành vi mặc định của S sẽ tinh chỉnh hành vi được ghi lại của T.
  • Hành vi thực tế của mã cho một lớp nên tinh chỉnh hành vi mặc định được ghi lại của nó.

@ user61852 nêu quan điểm rằng bạn có thể nêu trong chữ ký của phương thức rằng nó có thể đưa ra một ngoại lệ và chỉ cần thực hiện điều này (một cái gì đó không có mã hiệu ứng thực tế khôn ngoan), bạn không còn phá vỡ LSP.
Paul T Davies

@PaulTDavies Bạn nói đúng. Nhưng trong hầu hết các ngôn ngữ, chữ ký không phải là một cách tốt để tuyên bố rằng một thói quen có thể tạo ra một ngoại lệ. Ví dụ, trong OP (trong C #, tôi nghĩ) việc thực hiện thứ hai của Closethrow throw. Vì vậy, chữ ký tuyên bố rằng một ngoại lệ có thể được ném - nó không nói rằng một người sẽ không. Java làm một công việc tốt hơn trong vấn đề này. Mặc dù vậy, nếu bạn tuyên bố rằng một phương thức có thể khai báo một ngoại lệ, bạn nên ghi lại các trường hợp theo đó nó có thể (hoặc sẽ). Vì vậy, tôi lập luận rằng để chắc chắn về việc LSP có bị vi phạm hay không, chúng tôi cần tài liệu ngoài chữ ký.
Theodore Norvell

4
Rất nhiều câu trả lời ở đây dường như hoàn toàn bỏ qua thực tế là bạn không thể biết liệu hợp đồng có được xác nhận hay không nếu bạn không biết hợp đồng. Cảm ơn câu trả lời đó.
gnasher729

Câu trả lời tốt, nhưng các câu trả lời khác là tốt. Họ suy luận rằng lớp cơ sở không ném ngoại lệ vì không có gì trong lớp đó có dấu hiệu của điều đó. Vì vậy, chương trình sử dụng lớp cơ sở không nên chuẩn bị cho các trường hợp ngoại lệ.
inf3rno

Bạn đúng rằng danh sách ngoại lệ nên được ghi lại ở đâu đó. Tôi nghĩ rằng nơi tốt nhất là trong mã. Có một câu hỏi liên quan ở đây: stackoverflow.com/questions/16700130/ , Nhưng bạn có thể làm điều này mà không cần chú thích, v.v ... cũng vậy, chỉ cần viết một cái gì đó như if (false) throw new Exception("cannot start")vào lớp cơ sở. Trình biên dịch sẽ loại bỏ nó, và mã vẫn chứa những gì cần thiết. Btw. chúng tôi vẫn vi phạm LSP với các cách giải quyết này, vì điều kiện tiên quyết vẫn được tăng cường ...
inf3rno

6

Nó không vi phạm Nguyên tắc thay thế Liskov.

Nguyên tắc thay thế Liskov nói:

Hãy q (x) là một tài sản chứng minh về các đối tượng x kiểu T . Hãy S là một subtype của T . Loại S vi phạm Nguyên tắc thay thế Liskov nếu một đối tượng y loại S tồn tại, do đó q (y) không thể chứng minh được.

Lý do, tại sao việc triển khai tiểu loại của bạn không vi phạm Nguyên tắc thay thế Liskov, khá đơn giản: không có gì có thể được chứng minh về những gì Task::Close()thực sự làm. Chắc chắn, ProjectTask::Close()ném một ngoại lệ khi Status == Status.Started, nhưng vì vậy có thể Status = Status.Closedtrong Task::Close().


4

Vâng, đó là một vi phạm.

Tôi sẽ đề nghị bạn có thứ bậc của bạn ngược. Nếu không phải tất cả đều Taskcó thể đóng, thì close()không thuộc về Task. Có lẽ bạn muốn có một giao diện CloseableTaskmà tất cả những người không ProjectTasksthể thực hiện được.


3
Mọi nhiệm vụ đều có thể đóng, nhưng không phải trong mọi trường hợp.
Paul T Davies

Cách tiếp cận này có vẻ rủi ro đối với tôi vì mọi người có thể viết mã mong đợi tất cả các Nhiệm vụ triển khai ClosableTask, mặc dù nó mô hình chính xác vấn đề. Tôi bị giằng xé giữa cách tiếp cận này và một máy trạng thái vì tôi ghét máy trạng thái.
Jimmy Hoffa

Nếu Taskbản thân nó không thực hiện CloseableTaskthì họ đang thực hiện một cuộc gọi không an toàn ở đâu đó để gọi Close().
Tom G

@TomG đó là điều tôi sợ
Jimmy Hoffa

1
Đã có một máy trạng thái. Đối tượng không thể bị đóng vì nó ở trạng thái sai.
Kaz

3

Ngoài việc là một vấn đề LSP, có vẻ như nó đang sử dụng các ngoại lệ để kiểm soát luồng chương trình (tôi phải giả sử rằng bạn bắt gặp ngoại lệ tầm thường này ở đâu đó và thực hiện một số luồng tùy chỉnh thay vì để nó làm hỏng ứng dụng của bạn).

Có vẻ như đây là một nơi tốt để triển khai mẫu Trạng thái cho TaskState và để các đối tượng trạng thái quản lý các chuyển đổi hợp lệ.


1

Tôi đang thiếu ở đây một điều quan trọng liên quan đến LSP và Thiết kế theo Hợp đồng - trong các điều kiện tiên quyết, đó là người gọi có trách nhiệm đảm bảo các điều kiện tiên quyết được đáp ứng. Mã được gọi, theo lý thuyết DbC, không nên xác minh điều kiện tiên quyết. Hợp đồng sẽ chỉ định khi nào một tác vụ có thể được đóng (ví dụ CanClose trả về True) và sau đó mã gọi sẽ đảm bảo điều kiện tiên quyết được đáp ứng, trước khi nó gọi Đóng ().


Hợp đồng nên chỉ định bất kỳ hành vi nào mà doanh nghiệp cần. Trong trường hợp này, Close () đó sẽ đưa ra một ngoại lệ khi được gọi khi bắt đầu ProjectTask. Đây là một điều kiện hậu (nó nói những gì xảy ra sau khi phương thức được gọi) và hoàn thành nó là trách nhiệm của mã được gọi.
Goyo

@Goyo Có, nhưng như những người khác nói, ngoại lệ được nêu ra trong tiểu loại đã củng cố điều kiện tiên quyết và do đó đã vi phạm hợp đồng (ngụ ý) mà gọi Close () chỉ đơn giản là đóng nhiệm vụ.
Ezoela Vacca

Điều kiện tiên quyết nào? Tôi không nhìn thấy bất cứ gì.
Goyo

@Goyo Kiểm tra câu trả lời được chấp nhận, ví dụ :) Trong lớp cơ sở, Đóng không có điều kiện tiên quyết, nó được gọi và nó đóng nhiệm vụ. Tuy nhiên, ở trẻ, có một điều kiện tiên quyết về tình trạng không được bắt đầu. Như những người khác đã chỉ ra, đây là tiêu chí mạnh mẽ hơn và do đó hành vi không thể thay thế.
Ezoela Vacca

Không sao, tôi tìm thấy điều kiện tiên quyết trong câu hỏi. Nhưng sau đó, không có gì sai (DbC-khôn ngoan) với mã được gọi là kiểm tra các điều kiện trước và đưa ra các ngoại lệ khi chúng không được đáp ứng. Nó được gọi là "lập trình phòng thủ". Hơn nữa, nếu có một điều kiện hậu cho biết điều gì xảy ra khi điều kiện trước không được đáp ứng như trong trường hợp này, thì việc thực hiện phải xác minh điều kiện trước để đảm bảo rằng điều kiện hậu được đáp ứng.
Goyo

0

Có, đó là một vi phạm rõ ràng về LSP.

Một số người tranh luận ở đây rằng việc đưa ra rõ ràng trong lớp cơ sở rằng các lớp con có thể đưa ra ngoại lệ sẽ khiến điều này được chấp nhận, nhưng tôi không nghĩ đó là sự thật. Bất kể bạn viết tài liệu gì trong lớp cơ sở hoặc mức độ trừu tượng mà bạn di chuyển mã đến, các điều kiện tiên quyết vẫn sẽ được tăng cường trong lớp con, bởi vì bạn thêm phần "Không thể đóng Nhiệm vụ dự án bắt đầu" vào nó. Đây không phải là điều bạn có thể giải quyết bằng cách giải quyết, bạn cần một mô hình khác, không vi phạm LSP (hoặc chúng tôi cần nới lỏng các ràng buộc "điều kiện tiên quyết không thể bị hạn chế").

Bạn có thể thử mẫu trang trí nếu bạn muốn tránh vi phạm LSP trong trường hợp này. Nó có thể hoạt động, tôi không biết.

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