1. 개요

SOLID 설계 원칙은 Robert C. Martin이 2000년 논문인 Design Principles and Design Patterns 에서 소개했습니다 . SOLID 디자인 원칙은 유지 관리가 쉽고 이해하기 쉬우며 유연한 소프트웨어를 만드는 데 도움이 됩니다.

이 기사에서는 약어의 "L"인 Liskov 대체 원칙에 대해 설명합니다.

2. 개방/폐쇄 원칙

Liskov 대체 원리를 이해하려면 먼저 개방/폐쇄 원리(SOLID의 "O")를 이해해야 합니다.

개방/폐쇄 원칙의 목표는 우리가 소프트웨어를 설계하도록 장려하므로 새 코드를 추가해야만 새로운 기능을 추가할 수 있습니다 . 이것이 가능할 때 우리는 느슨하게 결합되어 쉽게 유지 관리할 수 있는 응용 프로그램을 갖게 됩니다.

3. 사용 사례 예시

개방형/폐쇄형 원칙을 좀 더 이해하기 위해 은행 애플리케이션 예제를 살펴보겠습니다.

3.1. 개방/폐쇄 원칙 없이

당사의 뱅킹 애플리케이션은 "현재" 및 "저축"의 두 가지 계정 유형을 지원합니다. 이들은 각각 CurrentAccountSavingsAccount 클래스로 표시됩니다 .

BankingAppWithdrawalService는 사용자 에게 인출 기능을 제공합니다.

1

안타깝게도 이 디자인을 확장하는 데 문제가 있습니다. BankingAppWithdrawalService는 account 의 두 가지 구체적인 구현을 알고 있습니다 . 따라서 새로운 계정 유형이 도입될 때마다 BankingAppWithdrawalService를 변경해야 합니다.

3.2. 개방형/폐쇄형 원칙을 사용하여 코드를 확장 가능하게 만들기

개방/폐쇄 원칙을 준수하도록 솔루션을 재설계해 보겠습니다. 새 계정 유형이 필요할 때 Account 기본 클래스를 대신 사용하여 수정에서 BankingAppWithdrawalService를 닫습니다 .

2

여기서는 CurrentAccountSavingsAccount가 확장하는 새로운 추상 Account 클래스를 도입했습니다 .

BankingAppWithdrawalService는 이상 구체적인 계정 클래스에 의존하지 않습니다. 이제 추상 클래스에만 의존하기 때문에 새 계정 유형이 도입될 때 변경할 필요가 없습니다.

결과적으로 BankingAppWithdrawalService 는 새 계정 유형이 있는 확장에 대해 열려 있지만 수정 에는 닫혀 있습니다 . 새 유형은 통합을 위해 변경할 필요가 없습니다.

3.3. 자바 코드

Java에서 이 예제를 살펴보겠습니다. 먼저 계정 클래스를 정의해 보겠습니다.

public abstract class Account {
    protected abstract void deposit(BigDecimal amount);

    /**
     * Reduces the balance of the account by the specified amount
     * provided given amount > 0 and account meets minimum available
     * balance criteria.
     *
     * @param amount
     */
    protected abstract void withdraw(BigDecimal amount);
}

그리고 BankingAppWithdrawalService 를 정의해 보겠습니다 .

public class BankingAppWithdrawalService {
    private Account account;

    public BankingAppWithdrawalService(Account account) {
        this.account = account;
    }

    public void withdraw(BigDecimal amount) {
        account.withdraw(amount);
    }
}

이제 이 설계에서 새 계정 유형이 어떻게 Liskov 대체 원칙을 위반할 수 있는지 살펴보겠습니다.

3.4. 새로운 계정 유형

은행은 이제 고객에게 고수익 고정 정기 예금 계좌를 제공하려고 합니다.

이를 지원하기 위해 새로운 FixedTermDepositAccount 클래스를 소개하겠습니다 . 현실 세계의 정기 예금 계좌는 일종의 계좌입니다. 이는 객체 지향 설계에서 상속을 의미합니다.

따라서 FixedTermDepositAccount를 Account 의 하위 클래스로 만들어 보겠습니다 .

public class FixedTermDepositAccount extends Account {
    // Overridden methods...
}

여태까지는 그런대로 잘됐다. 그러나 은행은 정기 예금 계좌에 대한 인출을 허용하지 않으려고 합니다.

이는 새 FixedTermDepositAccount 클래스가 Account가 정의하는 인출 방법을 의미 있게 제공할 수 없음을 의미합니다 . 이에 대한 한 가지 일반적인 해결 방법은 FixedTermDepositAccount가 이행할 수 없는 메서드에서 UnsupportedOperationException을 발생시키 도록 하는 것입니다.

public class FixedTermDepositAccount extends Account {
    @Override
    protected void deposit(BigDecimal amount) {
        // Deposit into this account
    }

    @Override
    protected void withdraw(BigDecimal amount) {
        throw new UnsupportedOperationException("Withdrawals are not supported by FixedTermDepositAccount!!");
    }
}

3.5. 새 계정 유형을 사용한 테스트

새 클래스가 잘 작동하는 동안 BankingAppWithdrawalService 와 함께 사용해 보겠습니다 .

Account myFixedTermDepositAccount = new FixedTermDepositAccount();
myFixedTermDepositAccount.deposit(new BigDecimal(1000.00));

BankingAppWithdrawalService withdrawalService = new BankingAppWithdrawalService(myFixedTermDepositAccount);
withdrawalService.withdraw(new BigDecimal(100.00));

당연히 뱅킹 애플리케이션이 다음 오류와 함께 충돌합니다.

Withdrawals are not supported by FixedTermDepositAccount!!

개체의 유효한 조합으로 인해 오류가 발생하는 경우 이 디자인에 분명히 문제가 있는 것입니다.

3.6. 무엇이 잘못되었나요?

BankingAppWithdrawalService 계정 클래스 의 클라이언트입니다 . Account 및 하위 유형 모두 Account 클래스가 인출 방법 에 대해 지정한 동작을 보장할 것으로 기대합니다 .

/**
 * Reduces the account balance by the specified amount
 * provided given amount > 0 and account meets minimum available
 * balance criteria.
 *
 * @param amount
 */
protected abstract void withdraw(BigDecimal amount);

그러나 인출 방법을 지원하지 않음으로써 FixedTermDepositAccount 는 이 방법 사양을 위반합니다 . 따라서 우리는 Account 를 FixedTermDepositAccount 로 안정적으로 대체할 수 없습니다 .

즉, FixedTermDepositAccount 는 Liskov 대체 원칙을 위반했습니다.

3.7. BankingAppWithdrawalService 의 오류를 처리할 수 없습니까 ?

Account인출 방법 의 클라이언트가 이를 호출할 때 발생할 수 있는 오류를 인식하도록 디자인을 수정할 수 있습니다. 그러나 이것은 클라이언트가 예기치 않은 하위 유형 동작에 대한 특별한 지식을 가지고 있어야 함을 의미합니다. 이것은 개방/폐쇄 원칙을 깨기 시작합니다.

즉, 개방/폐쇄 원칙이 제대로 작동하려면 클라이언트 코드를 수정하지 않고도 모든 하위 유형을 상위 유형으로 대체할 수 있어야 합니다 . Liskov 대체 원칙을 준수하면 이러한 대체 가능성이 보장됩니다.

이제 Liskov 대체 원리를 자세히 살펴보겠습니다.

4. Liskov 대체 원칙

4.1. 정의

Robert C. Martin은 다음과 같이 요약합니다.

하위 유형은 해당 기본 유형으로 대체 가능해야 합니다.

1988년에 이를 정의한 Barbara Liskov는 보다 수학적 정의를 제공했습니다.

S 유형의 각 객체 o1에 대해 T 유형의 객체 o2가 있고 T로 정의된 모든 프로그램 P에 대해 o1이 o2로 대체될 때 P의 동작이 변경되지 않으면 S는 T의 하위 유형입니다.

이러한 정의를 조금 더 이해해 봅시다.

4.2. 하위 유형이 상위 유형을 대체할 수 있는 경우는 언제입니까?

하위 유형은 해당 상위 유형을 자동으로 대체할 수 없습니다. 대체 가능하려면 하위 유형이 상위 유형처럼 작동해야 합니다 .

개체의 동작은 클라이언트가 의존할 수 있는 계약입니다. 동작은 공용 메서드, 해당 입력에 대한 제약 조건, 개체가 통과하는 모든 상태 변경 및 메서드 실행으로 인한 부작용에 의해 지정됩니다.

Java에서 하위 유형을 지정하려면 기본 클래스의 속성과 메서드가 하위 클래스에서 사용 가능해야 합니다.

그러나 동작 하위 유형 지정은 하위 유형이 상위 유형의 모든 메서드를 제공할 뿐만 아니라 상위 유형의 동작 사양을 준수해야 함을 의미합니다 . 이렇게 하면 상위 유형 동작에 대한 클라이언트의 모든 가정이 하위 유형에 의해 충족됩니다.

이것은 Liskov 대체 원칙이 객체 지향 설계에 가져오는 추가적인 제약입니다.

이제 이전에 발생한 문제를 해결하기 위해 은행 애플리케이션을 리팩터링해 보겠습니다.

5. 리팩토링

은행 예에서 발견한 문제를 해결하기 위해 근본 원인을 이해하는 것부터 시작하겠습니다.

5.1. 근본 원인

예제에서 FixedTermDepositAccount 는 Account 의 동작 하위 유형이 아닙니다 .

계정 설계는 모든 계정 유형이 인출을 허용한다고 잘못 가정했습니다. 결과적으로 인출을 지원하지 않는 FixedTermDepositAccount를  포함하여 Account 의 모든 하위 유형은 인출 방법을 상속했습니다.

Account 의 계약을 확장하여 이 문제를 해결할 수 있지만 대체 솔루션이 있습니다.

5.2. 수정된 클래스 다이어그램

계정 계층 구조를 다르게 설계해 보겠습니다.

삼

모든 계정이 인출을 지원하지 않기 때문에 인출 방법을 Account 클래스 에서 새로운 추상 하위 클래스 WithdrawableAccount 로 옮겼습니다 . CurrentAccountSavingsAccount 모두 출금이 가능합니다. 그래서 그들은 이제 새로운 WithdrawableAccount 의 하위 클래스로 만들어졌습니다 .

이는  BankingAppWithdrawalService가 인출 기능 을 제공하는 올바른 유형의 계정을 신뢰할 수 있음을 의미합니다 .

5.3. 리팩토링된 BankingAppWithdrawalService

BankingAppWithdrawalService는 이제 WithdrawableAccount를 사용해야 합니다 .

public class BankingAppWithdrawalService {
    private WithdrawableAccount withdrawableAccount;

    public BankingAppWithdrawalService(WithdrawableAccount withdrawableAccount) {
        this.withdrawableAccount = withdrawableAccount;
    }

    public void withdraw(BigDecimal amount) {
        withdrawableAccount.withdraw(amount);
    }
}

FixedTermDepositAccount 의 경우 Account 를 상위 클래스로 유지합니다 . 결과적으로 안정적으로 수행할 수 있는 입금 동작만 상속하고 더 이상 원하지 않는 인출 방법을 상속하지 않습니다. 이 새로운 디자인은 이전에 본 문제를 방지합니다.

6. 규칙

이제 제대로 작동하는 하위 유형을 만들기 위해 따르고 사용할 수 있는 메서드 서명, 불변량, 전제 조건 및 사후 조건과 관련된 몇 가지 규칙/기술을 살펴보겠습니다.

그들의 책 Program Development in Java: Abstraction, Specification, and Object-Oriented Design에서 Barbara Liskov와 John Guttag는 이러한 규칙을 서명 규칙, 속성 규칙 및 메서드 규칙의 세 가지 범주로 그룹화했습니다.

이러한 관행 중 일부는 이미 Java의 재정의 규칙에 의해 시행되고 있습니다.

여기서 몇 가지 용어에 주목해야 합니다. 넓은 유형이 더 일반적입니다.  예를 들어 객체는 모든 Java 객체를 의미할 수 있으며 CharSequence 보다 더 넓습니다 . 여기서 String 은 매우 구체적이므로 더 좁습니다.

6.1. 서명 규칙 - 메서드 인수 유형

이 규칙은 재정의된 하위 유형 메서드 인수 유형이 상위 유형 메서드 인수 유형과 동일하거나 더 넓을 수 있음을 나타냅니다 .

Java의 메서드 재정의 규칙은 재정의된 메서드 인수 유형이 상위 유형 메서드와 정확히 일치하도록 강제하여 이 규칙을 지원합니다.

6.2. 서명 규칙 – 반환 유형

재정의된 하위 유형 메서드의 반환 유형은 상위 유형 메서드의 반환 유형보다 좁을 수 있습니다 . 이를 반환 유형의 공분산 이라고 합니다 . 공분산은 상위 유형 대신 하위 유형이 허용되는 시기를 나타냅니다. Java는 반환 유형의 공분산을 지원합니다. 예를 살펴보겠습니다.

public abstract class Foo {
    public abstract Number generateNumber();    
    // Other Methods
}

Foo 의 generateNumber 메소드는 반환 유형Number 입니다 . 이제 더 좁은 유형의 Integer를 반환하여 이 메서드를 재정의해 보겠습니다 .

public class Bar extends Foo {
    @Override
    public Integer generateNumber() {
        return new Integer(10);
    }
    // Other Methods
}

Integer IS-A Number 때문에 Number를 기대하는 클라이언트 코드는 아무 문제 없이 Foo를 Bar바꿀 수 있습니다 .

반면에 Bar 의 재정의된 메서드가 Number 보다 더 넓은 유형 (예: Object ) 을 반환하는 경우 Object 의 하위 유형( 예: Truck ) 을 포함할 수 있습니다 . Number 반환 유형에 의존하는 클라이언트 코드는 Truck 을 처리할 수 없습니다 !

다행스럽게도 Java의 메서드 재정의 규칙은 재정의 메서드가 더 넓은 유형을 반환하는 것을 방지합니다.

6.3. 서명 규칙 – 예외

하위 유형 메서드는 상위 유형 메서드보다 더 적거나 더 좁은 예외를 throw할 수 있습니다(추가적이거나 더 넓은 범위는 아님) .

이는 클라이언트 코드가 하위 유형을 대체할 때 상위 유형 메소드보다 적은 수의 예외를 발생시키는 메소드를 처리할 수 있기 때문에 이해할 수 있습니다. 그러나 하위 유형의 메서드가 새롭거나 더 광범위한 확인된 예외를 throw하면 클라이언트 코드가 중단됩니다.

Java의 메서드 재정의 규칙은 이미 확인된 예외에 대해 이 규칙을 적용합니다. 그러나 Java의 재정의 메서드는 재정의된 메서드가 예외를 선언하는지 여부에 관계없이 모든 RuntimeException을 THROW할 수 있습니다.

6.4. 속성 규칙 - 클래스 불변성

클래스 불변은 개체의 모든 유효한 상태에 대해 참이어야 하는 개체 속성에 관한 주장입니다.

예를 살펴보겠습니다.

public abstract class Car {
    protected int limit;

    // invariant: speed < limit;
    protected int speed;

    // postcondition: speed < limit
    protected abstract void accelerate();

    // Other methods...
}

Car 클래스 속도가 항상 한계 미만이어야 한다는 클래스 불변성을 지정합니다 . 불변 규칙은 모든 하위 유형 메서드(상속 및 신규)가 상위 유형의 클래스 불변을 유지하거나 강화해야 한다고 명시합니다 .

클래스 불변성을 유지하는 Car 의 하위 클래스를 정의해 보겠습니다 .

public class HybridCar extends Car {
    // invariant: charge >= 0;
    private int charge;

      @Override
    // postcondition: speed < limit
    protected void accelerate() {
        // Accelerate HybridCar ensuring speed < limit
    }

    // Other methods...
}

이 예제에서 Car 의 불변량은 HybridCar 의 재정의된 가속 메서드 에 의해 보존됩니다 . HybridCar 자체 클래스 invariant charge >= 0 을 추가로 정의하며 이는 완벽하게 괜찮습니다.

반대로 클래스 불변이 하위 유형에 의해 유지되지 않으면 상위 유형에 의존하는 모든 클라이언트 코드가 중단됩니다.

6.5. 속성 규칙 – 히스토리 제약

기록 제약 조건은 하위 클래스 메서드(상속 또는 신규)가 기본 클래스가 허용하지 않은 상태 변경을 허용하지 않아야 한다고 명시합니다 .

예를 살펴보겠습니다.

public abstract class Car {

    // Allowed to be set once at the time of creation.
    // Value can only increment thereafter.
    // Value cannot be reset.
    protected int mileage;

    public Car(int mileage) {
        this.mileage = mileage;
    }

    // Other properties and methods...

}

Car 클래스 마일리지 속성 에 대한 제약 조건을 지정합니다 . 마일리지 속성 생성 시 한 번만 설정할 수 있으며 이후에는 재설정할 수 없습니다.

이제 Car를 확장하는 ToyCar를 정의해 보겠습니다 .

public class ToyCar extends Car {
    public void reset() {
        mileage = 0;
    }

    // Other properties and methods
}

ToyCar 에는 마일리지 속성을 재설정하는 추가 메서드 재설정이 있습니다 . 그렇게 함으로써 ToyCar는 부모가 마일리지 속성에 부과한 제약 조건을 무시했습니다. 이렇게 하면 제약 조건에 의존하는 모든 클라이언트 코드가 중단됩니다. 따라서  ToyCar 는 Car를 대체할 수 없습니다 .

마찬가지로 기본 클래스에 변경할 수 없는 속성이 있는 경우 하위 클래스는 이 속성이 수정되는 것을 허용하지 않아야 합니다. 이것이 불변 클래스가 final 이어야 하는 이유입니다 .

6.6. 방법 규칙 – 전제 조건

메서드를 실행하려면 전제 조건이 충족되어야 합니다 . 매개 변수 값과 관련된 전제 조건의 예를 살펴보겠습니다.

public class Foo {

    // precondition: 0 < num <= 5
    public void doStuff(int num) {
        if (num <= 0 || num > 5) {
            throw new IllegalArgumentException("Input out of range 1-5");
        }
        // some logic here...
    }
}

여기에서 doStuff 메소드 의 전제조건은 num 매개변수 값이 1과 5 사이여야 한다고 명시합니다. 메소드 내부의 범위 확인을 통해 이 전제조건을 적용했습니다. 하위 유형은 재정의하는 메서드의 전제 조건을 약화(강화는 아님)할 수 있습니다 . 하위 유형이 전제 조건을 약화시키면 상위 유형 방법에 의해 부과된 제약이 완화됩니다.

이제 약화된 전제 조건으로 doStuff 메서드를 재정의해 보겠습니다 .

public class Bar extends Foo {

    @Override
    // precondition: 0 < num <= 10
    public void doStuff(int num) {
        if (num <= 0 || num > 10) {
            throw new IllegalArgumentException("Input out of range 1-10");
        }
        // some logic here...
    }
}

여기서 전제 조건은 재정의된 doStuff  메서드 에서 0 < num <= 10 으로 약화되어 num 값의 범위를 넓힐 수 있습니다 . Foo.doStuff 에 유효한 모든 num 값은 Bar.doStuff 에도 유효합니다 . 결과적으로 Foo.doStuff 의 클라이언트는 Foo를 Bar대체할 때 차이점을 인지하지 못합니다 .

반대로 하위 유형이 전제 조건을 강화하는 경우(예: 이 예에서 0 < num <= 3 ) 상위 유형보다 더 엄격한 제한을 적용합니다. 예를 들어 num 의 값 4 및 5는 Foo.doStuff 에 유효 하지만 Bar.doStuff 에는 더 이상 유효하지 않습니다 .

이렇게 하면 이 새로운 더 엄격한 제약 조건을 기대하지 않는 클라이언트 코드가 중단됩니다.

6.7. 메소드 규칙 – 사후 조건

사후 조건 은 메서드가 실행된 후 충족되어야 하는 조건입니다.

예를 살펴보겠습니다.

public abstract class Car {

    protected int speed;

    // postcondition: speed must reduce
    protected abstract void brake();

    // Other methods...
}

여기서 Car  의 제동 방식은 방식 실행 종료 시  Car속도를 줄여야 한다는 사후 조건을 지정합니다 . 하위 유형은 재정의하는 메서드의 사후 조건을 강화(약화는 아님)할 수 있습니다 . 하위 유형이 사후 조건을 강화하면 상위 유형 방법보다 더 많은 것을 제공합니다.

이제 이 전제 조건을 강화하는 Car 의 파생 클래스를 정의해 보겠습니다.

public class HybridCar extends Car {

   // Some properties and other methods...

    @Override
    // postcondition: speed must reduce
    // postcondition: charge must increase
    protected void brake() {
        // Apply HybridCar brake
    }
}

HybridCar오버라이드 브레이크 방식은 추가로 충전량 증가시켜 사후 조건을 강화합니다. 결과적으로 Car 클래스에서 브레이크 메서드 의 사후 조건에 의존하는 모든 클라이언트 코드는 Car 를 HybridCar대체할 때 차이를 인식하지 못합니다 .

반대로 HybridCar가 오버라이드된 브레이크 방법 의 사후 조건을 약화시키면 더 이상 속도가 감소할 것이라고 보장할 수 없습니다 . 이로 인해 Car 대신 HybridCar 가 제공된 클라이언트 코드가 손상될 수 있습니다 .

7. 코드 냄새

실제 세계에서 상위 유형을 대체할 수 없는 하위 유형을 어떻게 식별할 수 있습니까?

Liskov 대체 원칙 위반의 징후인 몇 가지 일반적인 코드 악취를 살펴보겠습니다.

7.1. 하위 유형이 이행할 수 없는 동작에 대한 예외를 발생시킵니다.

앞서 뱅킹 애플리케이션 예제에서 이에 대한 예제를 보았습니다.

리팩토링 전에 Account 클래스에는 하위 클래스 FixedTermDepositAccount 가 원하지 않는 추가 인출 메서드가 있었습니다. FixedTermDepositAccount 클래스는 인출 방법 에 대해 UnsupportedOperationException을 발생시켜 이 문제를 해결 했습니다 . 그러나 이는 상속 계층 구조 모델링의 약점을 은폐하기 위한 해킹에 불과했습니다.

7.2. 하위 유형은 수행할 수 없는 동작에 대한 구현을 제공하지 않습니다.

이것은 위 코드 냄새의 변형입니다. 하위 유형은 동작을 수행할 수 없으므로 재정의된 메서드에서 아무 작업도 수행하지 않습니다.

여기에 예가 있습니다. FileSystem 인터페이스를 정의해 보겠습니다 .

public interface FileSystem {
    File[] listFiles(String path);

    void deleteFile(String path) throws IOException;
}

FileSystem을 구현하는 ReadOnlyFileSystem 을 정의해 보겠습니다 .

public class ReadOnlyFileSystem implements FileSystem {
    public File[] listFiles(String path) {
        // code to list files
        return new File[0];
    }

    public void deleteFile(String path) throws IOException {
        // Do nothing.
        // deleteFile operation is not supported on a read-only file system
    }
}

여기서 ReadOnlyFileSystem 은 deleteFile 작업을 지원하지 않으므로 구현을 제공하지 않습니다.

7.3. 클라이언트는 하위 유형에 대해 알고 있습니다.

클라이언트 코드가 instanceof 또는 downcasting을 사용해야 하는 경우  개방/폐쇄 원칙과 Liskov 대체 원칙이 모두 위반되었을 가능성이 있습니다.

FilePurgingJob 을 사용하여 이를 설명하겠습니다 .

public class FilePurgingJob {
    private FileSystem fileSystem;

    public FilePurgingJob(FileSystem fileSystem) {
        this.fileSystem = fileSystem;
    }

    public void purgeOldestFile(String path) {
        if (!(fileSystem instanceof ReadOnlyFileSystem)) {
            // code to detect oldest file
            fileSystem.deleteFile(path);
        }
    }
}

FileSystem 모델은 근본적으로 읽기 전용 파일 시스템과 호환되지 않기 때문에  ReadOnlyFileSystem은 지원할 수 없는 deleteFile 메서드를 상속합니다 . 이 예제 코드는 하위 유형 구현을 기반으로 특수 작업을 수행하기 위해 instanceof 검사를 사용합니다.

7.4. 하위 유형 메서드는 항상 같은 값을 반환합니다.

이것은 다른 것보다 훨씬 더 미묘한 위반이며 발견하기 어렵습니다. 이 예에서 ToyCar는 항상 잔여 연료 속성 에 대해 고정된 값을 반환합니다 .

public class ToyCar extends Car {

    @Override
    protected int getRemainingFuel() {
        return 0;
    }
}

인터페이스와 값이 의미하는 바에 따라 다르지만 일반적으로 객체의 변경 가능한 상태 값이 되어야 하는 것을 하드코딩하는 것은 하위 클래스가 상위 유형 전체를 충족하지 못하고 대체할 수 없다는 신호입니다.

8. 결론

이 기사에서는 Liskov Substitution SOLID 설계 원칙을 살펴보았습니다.

Liskov 대체 원칙은 좋은 상속 계층 구조를 모델링하는 데 도움이 됩니다. 이는 개방/폐쇄 원칙을 준수하지 않는 모델 계층 구조를 방지하는 데 도움이 됩니다.

Liskov 대체 원칙을 준수하는 모든 상속 모델은 암묵적으로 개방/폐쇄 원칙을 따릅니다.

먼저 Open/Closed 원칙을 따르려고 시도하지만 Liskov 대체 원칙을 위반하는 사용 사례를 살펴보았습니다. 다음으로 Liskov 대체 원칙의 정의, 행동 하위 유형의 개념 및 하위 유형이 따라야 하는 규칙을 살펴보았습니다.

마지막으로 기존 코드에서 위반 사항을 감지하는 데 도움이 되는 몇 가지 일반적인 코드 냄새를 살펴보았습니다.

항상 그렇듯이 이 기사의 예제 코드는 GitHub 에서 사용할 수 있습니다 .

Generic footer banner