이것이 Liskov 대체 원칙을 위반합니까?


132

Task 엔터티 목록과 ProjectTask하위 유형 이 있다고 가정 해보십시오 . ProjectTasks상태가 시작됨 인 경우 닫을 수없는 경우 를 제외하고 작업은 언제든지 닫을 수 있습니다 . UI는 시작을 닫는 옵션을 ProjectTask사용할 수 없도록해야 하지만 도메인에는 다음과 같은 보호 조치가 있습니다.

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();
     }
}

이제 Close()작업을 호출 할 ProjectTask때 시작 상태에 있고 기본 작업이 아닌 경우 호출이 실패 할 가능성이 있습니다. 그러나 이것은 비즈니스 요구 사항입니다. 실패해야합니다. 이것이 Liskov 대체 원칙을 위반 한 것으로 간주 될 수 있습니까 ?


14
liskov 치환 위반의 T 예에 적합합니다. 여기에서 상속을 사용하지 않으면 괜찮을 것입니다.
Jimmy Hoffa

8
다음과 같이 변경할 수 있습니다. public Status Status { get; private set; }; 그렇지 않으면 Close()방법을 해결할 수 있습니다.
Job

5
어쩌면이 예일 수도 있지만 LSP를 준수하는 데 중요한 이점은 없습니다. 나에게, 문제 의이 솔루션은 LSP를 준수하는 솔루션보다 명확하고 이해하기 쉽고 유지 관리하기 쉽습니다.
Ben Lee

2
@BenLee 유지하기가 쉽지 않습니다. 당신은 이것을 혼자서보고 있기 때문에 그렇게 보입니다. 시스템이 큰 경우 하위 유형이 Task다형성 코드에서 기괴한 비 호환성을 유발하지 않도록 Task하는 것은 큰 문제입니다. LSP는 변덕스럽지 않지만 대규모 시스템에서 유지 관리를 돕기 위해 정확하게 도입되었습니다.
Andres F.

8
@BenLee TaskCloser어떤 프로세스 가 있다고 상상해보십시오 closesAllTasks(tasks). 이 프로세스는 분명히 예외를 잡으려고 시도하지 않습니다. 결국,의 명시 적 계약의 일부가 아닙니다 Task.Close(). 이제 당신은 ProjectTask갑자기 TaskCloser예외를 던지기 시작했을 것입니다. 이것은 큰 문제입니다!
Andres F.

답변:


173

예, LSP를 위반 한 것입니다. 리스 코프 치환 원칙은 필요 있음

  • 하위 유형에서는 전제 조건을 강화할 수 없습니다.
  • 하위 유형에서는 사후 조건을 약화시킬 수 없습니다.
  • 상위 유형의 변형은 하위 유형으로 유지되어야합니다.
  • 히스토리 제한 사항 ( "히스토리 규칙"). 객체는 그 방법 (캡슐화)을 통해서만 수정 가능한 것으로 간주됩니다. 서브 타입은 수퍼 타입에 존재하지 않는 메소드를 도입 할 수 있기 때문에, 이러한 메소드의 도입은 수퍼 타입에 허용되지 않는 서브 타입의 상태 변경을 허용 할 수 있습니다. 히스토리 제한은이를 금지합니다.

예제에서는 Close()메소드 호출을위한 전제 조건을 강화하여 첫 번째 요구 사항을 위반합니다 .

강화 된 전제 조건을 상속 계층의 최상위 수준으로 가져 와서 고칠 수 있습니다.

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

의 호출이 있음을 명기하여 Close()경우에만 상태에서 유효 CanClose()수익률은 true당신이 사전 조건이 적용 할 Task받는 사람뿐만 아니라 ProjectTaskLSP를 위반 고정 :

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
나는 그 수표의 중복을 좋아하지 않습니다. Task.Close로 들어가는 예외를 선호합니다 .Close에서 Virtual을 제거하십시오.
Euphoric

4
@Euphoric 즉, 최상위 레벨 Close에서 점검을 수행하고 보호를 추가하는 DoClose것이 올바른 대안이 될 수 있습니다. 그러나 가능한 한 OP의 예에 가깝게 머물기를 원했습니다. 그것을 개선하는 것은 별도의 질문입니다.
dasblinkenlight

5
@Euphoric : 그러나 "이 작업을 닫을 수 있습니까?"라는 질문에 대답 할 방법이 없습니다. 닫으려고하지 않고 이로 인해 흐름 제어에 예외를 강제로 사용해야합니다. 그러나 나는 이런 종류의 일이 너무 멀리 갈 수 있음을 인정할 것입니다. 너무 멀리 가져 가면 이런 종류의 솔루션으로 인해 엔터프라이즈 혼란이 발생할 수 있습니다. 그럼에도 불구하고 OP의 질문은 원칙에 대해 더 많은 관심을 끌기 때문에 상아탑 답변이 매우 적합합니다. +1
Brian

30
@Brian The CanClose는 여전히 존재합니다. 작업을 닫을 수 있는지 확인하기 위해 여전히 호출 할 수 있습니다. 닫기의 체크인도 이것을 호출해야합니다.
Euphoric

5
@ 유포 릭 : 아, 나는 오해했다. 당신이 옳습니다, 그것은 훨씬 더 깨끗한 해결책을 만듭니다.
Brian

82

예. 이것은 LSP를 위반합니다.

내 제안은 CanClose기본 작업 에 방법 / 속성 을 추가 하는 것이므로 모든 작업은이 상태의 작업을 닫을 수 있는지 알 수 있습니다. 이유를 제시 할 수도 있습니다. 그리고에서 가상을 제거하십시오 Close.

내 의견을 바탕으로 :

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
이것에 감사드립니다. dasblinkenlight의 예를 한 단계 더 발전 시켰지만, 나는 그의 설명과 정당성을 좋아했습니다. 2 개의 답변을받을 수 없습니다.
Paul T Davies

서명이 공개 가상 부울 CanClose (out String reason) 인 이유를 알고 싶습니다. 아니면 내가 놓친 더 미묘한 것이 있습니까?
Reacher Gilt

3
@ReacherGilt 나는 당신이 무엇을 / 참조하는지 확인하고 내 코드를 다시 읽어야한다고 생각합니다. 당신은 혼란스러워합니다. 단순히 "작업이 종료되지 않으면 이유를 알고 싶습니다."
Euphoric

2
out은 모든 언어에서 사용할 수 없으며, 튜플을 반환하거나 이유를 포함하는 간단한 객체는 부울을 직접 갖는 용이성을 잃어 버렸음에도 불구하고 OO 언어에서 이식성이 향상됩니다. 이 답변에는 아무런 문제가 없습니다
Newtopian

1
그리고 CanClose 속성의 전제 조건을 강화해도 괜찮습니까? 즉 조건을 추가합니까?
John V

24

Liskov 대체 원칙에 따르면 기본 클래스는 프로그램의 바람직한 속성을 변경하지 않고 하위 클래스로 대체 할 수 있어야합니다. ProjectTask닫힐 때만 예외가 발생 하므로 이를 대체하기 위해 프로그램을 그에 맞게 변경해야 ProjectTask합니다 Task. 위반입니다.

그러나 Task서명을 닫을 때 예외가 발생할 수 있다고 서명을 수정 하면 원칙을 위반하지 않을 것입니다.


나는이 가능성이 없다고 생각하는 c #을 사용하지만 Java는 알고 있습니다.
Paul T Davies

2
@PaulTDavies msdn.microsoft.com/en-us/library/5ast78ax.aspx에서 예외를 발생시키는 방법으로 메서드를 장식 할 수 있습니다 . 기본 클래스 라이브러리에서 메소드 위로 마우스를 가져 가면 예외 목록이 표시됩니다. 적용되지는 않지만 그럼에도 불구하고 발신자를 인식하게합니다.
Despertar

18

LSP 위반에는 3 명이 필요합니다. T를 사용하지만 S의 인스턴스가 제공되는 Type T, Subtype S 및 프로그램 P

귀하의 질문에 T (작업) 및 S (프로젝트 작업)가 제공되었지만 P는 제공되지 않았으므로 귀하의 질문은 불완전하고 답변은 자격이 있습니다. 예외를 기대하지 않는 P가 있으면 해당 P에 대해 LSP 위반. 모든 P가 예외를 예상하면 LSP 위반이 없습니다.

그러나, 당신은 SRP 위반. 작업 상태가 변경 될 수 있다는 사실과 특정 상태의 특정 작업 이 다른 상태로 변경 되어서는 안된다는 정책 은 매우 다른 책임입니다.

  • 책임 1 : 작업을 나타냅니다.
  • 책임 2 : 작업 상태를 변경하는 정책을 구현하십시오.

이 두 가지 책임은 다른 이유로 바뀌므로 별도의 수업에 있어야합니다. 작업은 작업의 사실과 작업과 관련된 데이터를 처리해야합니다. TaskStatePolicy는 주어진 응용 프로그램에서 작업이 상태에서 상태로 전환되는 방식을 처리해야합니다.


2
책임은 도메인과 (이 예제에서) 복잡한 작업 상태와 그 변경자가 얼마나 많은지에 달려 있습니다. 이 경우에는 그러한 표시가 없으므로 SRP에는 문제가 없습니다. LSP 위반에 관해서는, 우리 모두는 호출자가 예외를 기대하지 않으며 응용 프로그램이 잘못된 상태가되는 대신 합리적인 메시지를 표시해야한다고 생각합니다.
행복감

Unca 'Bob이 응답합니까? "우리는 합당하지 않다! 우리는 합당하지 않다!" 어쨌든 ... 모든 P가 예외를 기대하면 LSP 위반이 없습니다. 그러나 T 인스턴스를 규정하면 OpenTaskException(힌트, 힌트)를 던질 수 없으며 모든 P가 예외기대하면 구현이 아닌 인터페이스 에 대한 코드에 대해 무엇을 말 합니까? 내가 무슨 소리 야? 모르겠어요 나는 Unca 'Bob의 대답에 대해 언급하고 있다는 재즈를 당했다.
radarbob

3
LSP 위반을 증명하는 데 세 가지 개체가 필요하다는 것이 맞습니다. 그러나 S가 없을 때 정확하지만 S를 추가해도 실패하는 프로그램 P가있는 경우 LSP 위반이 존재합니다.
kevin cline

16

이것은 LSP를 위반 하거나 위반 하지 않을 수 있습니다.

진심으로. 내 말 들어

LSP를 따르는 경우 유형의 ProjectTask객체 Task가 작동 할 것으로 예상되는 유형의 객체가 작동해야 합니다.

코드의 문제점은 유형의 객체가 어떻게 Task동작 할 것인지 문서화하지 않았다는 것 입니다. 코드를 작성했지만 계약은 없습니다. 에 대한 계약을 추가하겠습니다 Task.Close. 내가 추가 한 계약에 따라 ProjectTask.CloseLSP를 따르거나 따르지 않는 코드가 있습니다.

Task.Close에 대한 다음 계약을 감안할 때의 코드 는 LSP를 따르지 ProjectTask.Close 않습니다 .

     // 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;
     }

Task.Close에 대한 코드에 대해 다음과 계약을 감안할 ProjectTask.Close 않습니다 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;
     }

재정의 될 수있는 메소드는 다음 두 가지 방법으로 문서화해야합니다.

  • "Behaviour"는 수신자 객체가 Task임을 알고 있지만 어떤 클래스가 직접적인 인스턴스인지 는 모르는 클라이언트가 신뢰할 수있는 것을 문서화합니다 . 또한 서브 클래스의 디자이너에게 어떤 재정의가 합리적이고 합리적이지 않은지를 알려줍니다.

  • "기본 동작"은 수신자 객체가 직접 인스턴스라는 것을 알고있는 클라이언트가 신뢰할 수있는 것을 문서화합니다 Task(즉, 사용하는 경우 얻는 것) new Task(). 메소드를 대체하십시오.

이제 다음 관계가 유지되어야합니다.

  • S가 T의 하위 유형 인 경우 S의 문서화 된 동작은 T의 문서화 된 동작을 세분화해야합니다.
  • S가 T의 하위 유형 인 경우 S의 코드 동작은 문서화 된 T 동작을 세분화해야합니다.
  • S가 T의 하위 유형 (또는 같음) 인 경우 S의 기본 동작은 문서화 된 T의 동작을 세분화해야합니다.
  • 클래스 코드의 실제 동작은 문서화 된 기본 동작을 개선해야합니다.

@ user61852는 메소드의 서명에서 예외를 일으킬 수 있다고 언급 할 수 있다는 점을 제기했으며 단순히이를 수행함으로써 (실제 효과 코드가 현명하지 않은 것) 더 이상 LSP를 위반하지 않습니다.
Paul T Davies

@PaulTDavies 당신이 맞아요. 그러나 대부분의 언어에서 서명은 루틴이 예외를 발생시킬 수 있음을 선언하는 좋은 방법이 아닙니다. 예를 들어 OP (C #에서는)의 두 번째 구현 Close이 throw 라고 생각합니다 . 따라서 서명은 예외 가 발생할 수 있음을 선언하지만 예외 발생하지 않습니다. Java는 이와 관련하여 더 나은 작업을 수행합니다. 그럼에도 불구하고, 메소드가 예외를 선언 할 수 있다고 선언하는 경우, 예외가 발생할 수있는 상황을 문서화해야합니다. 따라서 LSP 위반 여부를 확인하려면 서명 이외의 문서가 필요하다고 주장합니다.
Theodore Norvell

4
여기에 많은 대답은 계약을 모르는 경우 계약이 유효한지 알 수 없다는 사실을 완전히 무시하는 것 같습니다. 그 답변에 감사드립니다.
gnasher729

좋은 답변이지만 다른 답변도 좋습니다. 그들은 그 클래스에 그 징후를 나타내는 아무것도 없기 때문에 기본 클래스가 예외를 던지지 않는다고 추론합니다. 따라서 기본 클래스를 사용하는 프로그램은 예외를 준비하지 않아야합니다.
inf3rno

예외 목록은 어딘가에 문서화해야합니다. 가장 좋은 장소는 코드에 있다고 생각합니다. 여기에 관련 질문이 있습니다 : stackoverflow.com/questions/16700130/ ... 그러나 주석 없이이 작업을 수행 할 수 있습니다 ... if (false) throw new Exception("cannot start")기본 클래스 와 비슷한 것을 작성하십시오. 컴파일러는 그것을 제거하지만 여전히 코드에는 필요한 것이 포함되어 있습니다. Btw. 전제 조건이 여전히 강화 되었기 때문에 이러한 해결 방법으로 LSP 위반이 계속 발생합니다.
inf3rno

6

Liskov 대체 원칙을 위반하지 않습니다.

Liskov 대체 원칙은 다음과 같이 말합니다.

하자 Q (x)는 객체에 대한 증명 속성 수 X 타입의 T . ST 의 하위 유형 이라고합시다 . q (y) 를 입증 할 수 없도록 S 유형 의 객체 y 가 존재 하는 경우 유형 S 는 Liskov 대체 원칙을 위반합니다 .

하위 유형의 구현이 Liskov 대체 원칙을 위반하지 않는 이유는 매우 간단합니다. Task::Close()실제로 수행하는 작업 에 대해 입증 할 수있는 것은 없습니다 . 물론, ProjectTask::Close()경우에 예외를 throw Status == Status.Started하지만 그렇게 수도 Status = Status.Closed에서와 Task::Close().


4

예, 위반입니다.

나는 당신에게 당신의 계층 구조가 거꾸로 있다고 제안합니다. 모든 Task것이 가까울 수 없다면에 close()속하지 않습니다 Task. 아마도 당신 CloseableTask은 모두 ProjectTasks구현할 수 없는 인터페이스를 원할 것입니다.


3
모든 작업은 마감이 가능하지만 모든 상황에서 가능한 것은 아닙니다.
Paul T Davies

사람들이 모든 Tasks가 ClosableTask를 구현하기를 기대하는 코드를 작성할 수 있기 때문에이 접근법은 위험합니다. 상태 머신을 싫어하기 때문에이 접근법과 상태 머신 사이에서 찢어졌습니다.
Jimmy Hoffa

경우 Task자체는 구현하지 않는 CloseableTask그들은 안전하지 않은 캐스트를하고있는 곳도 호출합니다 Close().
Tom G

@TomG 그것이 제가 두려워하는 것입니다
Jimmy Hoffa

1
상태 머신이 이미 있습니다. 상태가 잘못되어 개체를 닫을 수 없습니다.
Kaz

3

LSP 문제 외에도 예외를 사용하여 프로그램 흐름을 제어하는 ​​것처럼 보입니다 (이 사소한 예외를 잡아서 앱을 중단시키지 않고 사용자 정의 흐름을 수행한다고 가정해야합니다).

TaskState에 대한 State 패턴을 구현하고 상태 객체가 유효한 전환을 관리 할 수있는 좋은 장소 인 것 같습니다.


1

LSP 및 계약에 의한 디자인과 관련된 중요한 사항이 누락되었습니다. 전제 조건에서 전제 조건을 충족시키는 책임은 호출자입니다. DbC 이론에서 호출 된 코드는 전제 조건을 검증하지 않아야합니다. 계약은 작업을 닫을 수있는시기 (예 : CanClose가 True를 반환)를 지정한 다음 호출 코드는 Close ()를 호출하기 전에 사전 조건이 충족되는지 확인해야합니다.


계약은 비즈니스에 필요한 모든 행동을 명시해야합니다. 이 경우 Close ()는 시작시 호출 될 때 예외를 발생 ProjectTask시킵니다. 이것은 사후 조건 ( 메소드가 호출 된 발생하는 상황 나타냄)이며이를 수행하는 것은 호출 된 코드의 책임입니다.
Goyo

@Goyo 네, 그러나 다른 사람들이 말했듯이 전제 조건을 강화하는 하위 유형에서 예외가 발생하여 Close () 호출이 단순히 작업을 닫는 (암시 적) 계약을 위반했습니다.
Ezoela Vacca

어떤 전제 조건? 나는 아무것도 보지 못한다.
Goyo

@Goyo 허용되는 답변을 확인하십시오 (예 : :) 기본 클래스에서 Close에는 전제 조건이 없으며 호출되며 작업을 닫습니다. 그러나 어린이에게는 상태가 시작되지 않음에 대한 전제 조건이 있습니다. 다른 사람들이 지적했듯이, 이것은 더 강한 기준이며 행동은 대체 할 수 없습니다.
Ezoela Vacca

걱정하지 마십시오. 질문에서 전제 조건을 찾았습니다. 그러나 호출 된 코드가 사전 조건을 확인하고 충족되지 않을 때 예외를 발생시키는 데는 아무런 문제가 없습니다 (DbC 방식). 이것을 "방어 프로그래밍"이라고합니다. 또한,이 경우와 같이 전제 조건이 충족되지 않을 때 발생하는 사후 조건이있는 경우, 구현은 사후 조건이 충족되도록하기 위해 전제 조건을 검증해야한다.
Goyo

0

예, LSP를 명백히 위반하는 것입니다.

어떤 사람들은 기본 클래스에서 서브 클래스가 예외를 던질 수 있다고 명시 적으로 만드는 것이 이것을 받아 들일 수 있다고 주장하지만, 나는 그것이 사실이라고 생각하지 않습니다. 기본 클래스에서 무엇을 문서화하거나 코드를 어떤 추상화 수준으로 이동하든 "시작된 프로젝트 작업을 닫을 수 없습니다"부분을 추가하기 때문에 하위 클래스에서 사전 조건이 여전히 강화됩니다. 이것은 해결 방법으로 해결할 수있는 것이 아니며 LSP를 위반하지 않는 다른 모델이 필요합니다 (또는 "전제 조건을 강화할 수 없음"제약 조건에서 완화해야 함).

이 경우 LSP 위반을 피하려면 데코레이터 패턴을 사용해보십시오. 잘 모르겠습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.