고체: 리스코프 교체 원리

이것은 이 시리즈의 연속이다.
리스코프 교체 원칙은 모든 원칙 중 가장 기술적인 원칙이다.그러나 이것은 결합 해제 응용 프로그램을 개발하는 데 가장 도움이 되는 응용 프로그램으로, 이것은 다시 사용할 수 있는 구성 요소를 설계하는 기초이다.
이 원칙에 대한 Barbara Liskov의 정의는 다음과 같습니다.

Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T”


Liskov가 제시한 정의는 Bertrand Meyer의 정의를 바탕으로 하는 계약 설계(DbC)입니다.선결 조건, 불변량 및 후결 조건을 통해 확정된 계약:
  • 루틴은 호출된 클라이언트 모듈이 들어갈 때 어떤 조건을 보장할 수 있기를 기대할 수 있습니다. 루틴의 선결 조건입니다.이것은 고객의 의무이자 공급업체의 이익이다. 왜냐하면 이것은 전제조건 이외의 사건을 처리할 필요가 없기 때문이다.
  • 예행 절차는 퇴출 시 일정한 재산을 보장할 수 있다. 예행 절차의 후 조건인 공급업체에 대한 의무, 고객에 대한 이익이다.
  • 특정한 속성을 유지하고 들어갈 때 가설하고 종료할 때 클래스 불변량을 보증한다.
  • 계약과 실현의 개념은 대상 프로그래밍에서의 계승과 다태성의 기초이다.
    1996년에 Robert C.Martin은 Liskov가 제시한 개념을 다음과 같이 재정의했다.

    Function that use pointers of references to base classes must be able to use objects of derived classes without knowing it.


    Bob Martin이 제시한 재정의는 Liskov가 수년 전에 실현한 개념을 간소화하고 개발자가 이를 채택하는 데 도움이 된다.

    리스코프 대체 원칙을 어기다
    은행 실체의 개발자로서 당신은 은행 계좌를 관리하는 시스템을 실현해야 합니다.당신의 사장은 프로젝트의 첫 번째 돌진 단계에서 기본과 고급 은행 계좌를 관리하는 시스템을 실시할 것을 요구합니다.그것들 사이의 차이는 후자가 어떤 예금에 대한 선호점을 축적했다는 데 있다.
    다음과 같은 추상류를 실현하는 것을 체계적인 기초로 한다.
    public abstract class BankAccount {
    
        /**
         * In charge of depositing a specific amount into the account.
         * @param amount            Dollar ammount.
         */
        public abstract void deposit(double amount);
    
        /**
         * In charge of withdrawing a specific amount from the account.
         * @param amount            Dollar amount.
         * @return                  Boolean result.
         */
        public abstract boolean withdraw(double amount);
    }
    
    이 추상 클래스는 모든 파생 클래스가 BankAccount 클래스에서 정의한 추상 방법을 다시 쓰는 의무를 정의합니다.이것은 기본 계좌와 보험료 계좌가 반드시 예금과 인출 방식보다 우선해야 한다는 것을 의미한다.
    public class BasicAccount extends BankAccount {
    
        private double balance;
    
        @Override
        public void deposit(double amount) {
            this.balance += amount;
        }
    
        @Override
        public boolean withdraw(double amount) {
            if(this.balance < amount)
                return false;
            else{
                this.balance -= amount;
                return true;
            }       
        }
    }
    
    public class PremiumAccount extends BankAccount {
    
        private double balance;
        private int preferencePoints;
    
        @Override
        public void deposit(double amount) {
            this.balance += amount;
            accumulatePreferencePoints();
        }
    
        @Override
        public boolean withdraw(double amount) {
             if(this.balance < amount)
                return false;
            else{
                this.balance -= amount;
                accumulatePreferencePoints();
                return true;
            }
        }
    
        private void accumulatePreferencePoints(){
            this.preferencePoints++;
        }
    
    }
    
    이러한 클래스 중 어느 것이든 생산 환경에 대한 최소한의 검증을 고려하십시오.
    모든 기본과 고급 계좌의 관리 비용은 매년 25달러의 할인을 받는다.이 정책을 구현하려면 다음 클래스를 정의합니다.
    public class WithdrawalService {
    
        public static final double ADMINISTRATIVE_EXPENSES_CHARGE = 25.00;
    
        public void cargarDebitarCuentas(){
    
            BankAccount basiAcct = new BasicAccount();
            basiAcct.deposit(100.00);
    
            BankAccount premiumAcct = new PremiumAccount();
            premiumAcct.deposit(200.00);
    
            List<BankAccount> accounts = new ArrayList();
    
            accounts.add(basiAcct);
            accounts.add(premiumAcct);
    
            debitAdministrativeExpenses(accounts);
    
        }
    
        private void debitAdministrativeExpenses(List<BankAccount> accounts){
            accounts.stream()
                    .forEach(account -> account.withdraw(WithdrawalService.ADMINISTRATIVE_EXPENSES_CHARGE));
        }
    }
    
    프로젝트의 두 번째 돌진 단계에서 사장은 너에게 은행 계좌 관리 시스템에서 장기 계좌를 실현하라고 요구한다.장기 계좌와 기본/프리미엄 계좌 간의 차이는 다음과 같다.
  • 장기 계좌는 관리 비용을 면제한다.
  • 장기 계좌에서는 인출을 허락하지 않습니다.고객이 계좌 중의 어떤 금액을 인출하려면 반드시 다른 절차를 통해 진행해야 한다.
  • 계정 시스템을 담당하는 개발자로서 BankAccount 클래스를 장기 계정으로 확장하기로 결정했습니다.
    public class LongTermAccount extends BankAccount {
    
        private double balance;
    
        @Override
        public void deposit(double amount) {
            this.balance += amount;
        }
    
        @Override
        public boolean withdraw(double amount) {
            throw new UnsupportedOperationException("Not supported yet."); 
        }
    }
    
    이 부분은 리스코프 대체 원칙을 분명히 위반했다.인출 방법을 다시 쓰지 않으면 LongerAccount의 BankAccount 클래스를 확장할 수 없습니다.그러나 장기 계좌는 항목의 요구에 따라 인출을 허용하지 않는다.
    다음 두 가지 옵션으로 이 문제를 해결할 수 있습니다.
  • 철회 방법을 빈 방법으로 다시 쓸 수도 있고, UnsupportedOperationException을 던질 수도 있다.그러나 BankAccount 대상은 LongterAccount 대상과 완전히 바꿀 수 없습니다. 왜냐하면 우리가 인출 방법을 실행하려고 시도하면 이상이 발생하기 때문입니다.이 문제의 해결 방안으로서, 우리는 debit Administrative Expenses 방법을 조정할 수 있다. 그러면 우리는 Longer Account 대상을 뛰어넘을 수 있지만, 이것은 개방/폐쇄 원칙을 위반할 것이다.예를 들면 다음과 같습니다.
  • private void debitAdministrativeExpenses(List<BankAccount> accounts){
    
            for(BankAccount account : accounts){
                if(account instanceof LongTermAccount)
                    continue;
                else
                    account.withdraw(ADMINISTRATIVE_EXPENSES_CHARGE);
            }
        }
    
  • 코드를 Liskov 교체 지침에 맞게 만들 수 있습니다.

  • Liskov 교체 원리 구현
    은행 계좌 구조의 주요 문제는 장기 계좌는 일반적인 은행 계좌가 아니라 적어도 뱅크 어카운트 추상류에서 정의된 유형이 아니라는 것이다.소인 추리 영역에는 하나의 종류가 'X' 유형의 하위 유형인지 검사하는 간단한 테스트가 있다.오리 테스트에 따르면 "오리처럼 보이고 수영은 오리처럼 보이며 거리는 소리는 오리처럼 보인다면 오리일 가능성이 높다"고 한다.장기 계좌는 일반적인 은행 계좌처럼 보이지만, 그 행위는 일반적인 은행 계좌처럼 보이지 않는다.이 문제를 해결하기 위해서 우리는 현재의 학급 구조를 바꿔야 한다.
    코드가 LSP와 일치하도록 다음과 같이 변경합니다.
  • 모든 유형의 은행 계좌는 예금 조작을 허용한다.
  • 기본과 고급 은행 계좌만 인출 조작을 허용한다.
  • 우리는 모든 유형의 계좌에 추상적인 은행 계좌를 정의할 것이다.이 추상류는 예금 방법 하나만 정의할 것이다.
  • 우리는 Drankable Account 추상 클래스로 Bank Account를 확장할 것이다. 이 추상 클래스는 차용 방법을 정의할 것이다.
  • 기본 계좌와 고급 계좌는 인출 계좌 추상류를 확장하고 장기 계좌는 은행 계좌 추상류를 확장한다.
  • 추상적인 뱅크어카운트류는 예금 방법을 정의할 것이다.
    public abstract class BankAccount {
    
        /**
         * In charge of depositing a specific amount into the account.
         * @param amount            Dollar ammount.
         */
        public abstract void deposit(double amount);
    }
    
    추상적인 인출 계좌류는 인출 방법을 정의할 것이다.
    public abstract class WithdrawableAccount extends BankAccount {
    
        /**
         * In charge of withdrawing a specific amount from the account.
         * @param amount            Dollar amount.
         * @return                  Boolean result.
         */
        public abstract boolean withdraw(double amount);
    }
    
    기본 및 고급 계정 클래스는 BankAccount 클래스를 확장하는 인출 계정 클래스를 확장합니다.이런 플러그인 상속은 기본/고급 계좌가 예금과 인출의 두 가지 방법을 동시에 가지도록 허용한다.
    public class BasicAccount extends WithdrawableAccount {
    
        private double balance;
    
        @Override
        public void deposit(double amount) {
            this.balance += amount;
        }
    
        @Override
        public boolean withdraw(double monto) {
            if(this.balance < monto)
                return false;
            else{
                this.balance -= monto;
                return true;
            }       
        }   
    }
    
    public class PremiumAccount extends WithdrawableAccount {
    
        private double balance;
        private int preferencePoints;
    
        @Override
        public void deposit(double monto) {
            this.balance += monto;
            accumulatePreferencePoints();
        }
    
        @Override
        public boolean withdraw(double monto) {
             if(this.balance < monto)
                return false;
            else{
                this.balance -= monto;
                accumulatePreferencePoints();
                return true;
            }
        }
    
        private void accumulatePreferencePoints(){
            this.preferencePoints++;
        }
    }
    
    인출 서비스 유형은 인출 계좌 유형이나 하위 유형으로만 이루어진다.
    public class WithdrawableService {
    
        public static final double ADMINISTRATIVE_EXPENSES_CHARGE = 25.00;
    
        public void cargarDebitarCuentas(){
    
            WithdrawableAccount basicAcct = new BasicAccount();
            basicAcct.deposit(100.00);
    
            WithdrawableAccount premiumAcct = new PremiumAccount();
            premiumAcct.deposit(200.00);
    
            List<WithdrawableAccount> accounts = new ArrayList();
    
            accounts.add(basicAcct);
            accounts.add(premiumAcct);
    
            debitarGastosAdmon(accounts);
    
        }
    
        private void debitarGastosAdmon(List<WithdrawableAccount> accounts){
            accounts.stream()
                    .forEach(account -> account.withdraw(WithdrawableService.ADMINISTRATIVE_EXPENSES_CHARGE));
        }
    }
    
    클래스 구조를 변경하면 코드가 LSP와 일치하는지 확인합니다.이제 LongterAccount 클래스에서 추출 방법을 사용할 필요가 없습니다.그 밖에 우리는 인출 계좌의 대상과 이 추상적인 종류의 모든 하위 유형을 교환할 것이다.클래스 구조도 OCP가 호환됩니다. 만약에 우리가 인출을 허용하지 않는 다른 은행 계좌 유형을 추가하면 현재 코드를 수정하여 은행 계좌 유형을 확장할 필요가 없습니다.

    대체 원리의 중요성
    LSP를 사용하면 설계 단계에서 오류의 범위를 파악하고 수정할 수 있습니다.Liskov 교체 원칙은 자바 기업판과 스프링 프레임워크에서 널리 사용되는 의존 주입 개념을 개발하는 기초이다.
    Liskov 대체 원칙을 위반하는 경우를 쉽게 발견하려면 다음 프롬프트를 사용합니다.
    객체의 유형 도입 조건을 사용하는 경우 위의 instanceof 예와 같이 LSP 충돌이 발생합니다.
    추상 클래스를 확장하고 그 중 하나를 빈 방법으로 설정하거나 정의되지 않은 이상을 던지면 LSP 충돌이 발생합니다.

    왜 추상류가 아닌 인터페이스를 사용하지 않습니까
    인터페이스와 추상류는 매우 비슷하지만 그것들은 다르다.추상 클래스는 하위 클래스가 반드시 실현해야 하는 기능을 정의한다.다른 한편, 인터페이스는 주어진 인터페이스의 모든 종류를 실현해야 하는 기능을 정의했다.
    예를 들어 추상적인 정의에서 말이 나는 것은 말의 일종이기 때문이다.그에 비해 인터페이스는'사물'을'이동할 수 있다'고 정의한다. 인터페이스를 실현함으로써 사물이 반드시 이동해야 한다는 약속이 존재하기 때문이다.
    이것은 두 가지 문제를 불러일으켰다.
  • 우리는 인터페이스를 사용하여 은행 계좌의 예시를 실현할 수 있습니까?네, 그럼요.여기에는 틀린 답안이 없다.추상적인 클래스나 인터페이스를 사용해서 이 예시를 실현할 수 있습니다.
  • LSP를 위반하는 인터페이스도 사용할 수 있습니까?
    응, 여기는 정답이 없어.LSP는 파팅 배경에서 정의됩니다.그러나 나는 이것이 더 이상 효과가 없다고 생각한다.LSP는 추상 클래스나 인터페이스가 아니라 계약 이행에 관한 것입니다.
  • LSP에 대해 더 많은 정보를 알고 싶으면 Uncle Bob’s Blog를 보십시오.
    다음 글에서 우리는 인터페이스 격리 원칙을 토론할 것이다.
    너는 나를 따라올 수도 있고, 나를 따라올 수도 있다.

    좋은 웹페이지 즐겨찾기