본문 바로가기

IT Book Summary/Object: 객체지향설계

Appendix A 계약에 의한 설계 Design By Contract

메서드 구현이 복잡하다면 인터페이스 만으로 객체의 행동을 파악하기 어려움.

명령의 부수효과를 쉽게 표현할 수 있는 커뮤니케이션 수단이 필요함.

 

계약의 의한 설계를 사용해 협력에 필요한 제약과 부수효과를 정의하고 문서화 할 수 있음.

-> 실행 가능한 검증도구

 


01 협력과 계약

 

부수효과를 명시적으로

 

메시지와 파라미터는 시그니처를 통해 전달할 수 있지만 협력을 위한 약속과 제약은 인터페이스를 통해 전달 못하기 때문에

협력과 관련된 내용이 암시적으로 남음.

 

ex) 6장의 명령-쿼리 분리 원칙 , 일정관리 프로그램

 

class Event
{
//RecurringSchedule의 조건을 만족시키는지 여부 확인후 Reschedule 메서드 호출
    public bool IsSatisfied(RecurringSchedule schedule){ ... }
    
    public void Reschedule(RecurringSchedule schedule)
    {
        Contract.Requires(IsSatisfied(schedule));
        ...
    }
}

 

if 문으로 확인하는 것과 다른 것은??

- 정합성 체크 로직의 문서화

 

계약에 의한 설계 라이브러리 Code Contracts 사용하면 제약조건 명시적으로 표현하는것이 가능.

실행을 통해 검증도 가능

 

 

계약

 

  • 각 계약 당사자는 계약으로부터 이익 을 기대하고 의무를 이행한다.
  • 각 계약 당사자의 이익과 의무는 계약서에 문서화 된다.

한쪽의 의무가 반대쪽의 권리가 됨.

계약은 협력을 명확하게 정의하고 커뮤니케이션할 수 있는 범용적 아이디어

 


02 계약에 의한 설계

 

버트란드 마이어가 제시한 계약의 개념

  • 협력에 참여하는 각 객체는 계약으로부터 이익을 기대하고 이익을 얻기 위해 의무를 이행한다.
  • 협력에 참여하는 각 객체의 이익과 의무는 객체의 인터페이스 상에 문서화 된다.

'인터페이스에 대해 프로그래밍하라' 원칙의 확장

 

6장에서 설명한 '의도를 드러내는 인터페이스'를 만들어서 협력을 위해 수행해야하는 제약조건을 명시할 수 있다.

 

유효 범위의 전달값, 기대하는 반환값, 메시지 전송 전과 후의 정상적 서버의 상태

 -> 계약에 의한 설계를 구성하는 세가지 요소

  • 사전조건 precondition : 메서드가 호출되기 위해 만족되어야 하는 조건. 메서드의 요구사항 명시. 만족시켜야 하는 클라이언트의 의무
  • 사후조건 postcondition : 메서드가 실행후 클라이언트에 보장해야 하는 조건. 만족시켜야하는 서버의 의무.
  • 불변식 invariant : 보장되어야 하는 서버의 조건. 메서드를 실행하기 전이나 종료된 후에 항상 참 이어야 함.

자바의 경우 언어차원에서 계약에 의한 설계 개념을 지원하지 않으므로

닷넷 프레임웍 Code Contracts 이용해 예제를 보자.

 

 

사전조건

 

메서드가 정상적으로 실행되기 위해 만족해야하는 조건. 클라이언트의 의무.

일반적으로 인자의 정합성을 체크하기 위해 사용.

 

만족시키지 못할 경우 예외 발생.

 

 

사후조건

 

실행 결과가 올바른지 검사 실행 후 객체가 유효한 상태인지 검증. 서버의 의무.

 

  • 인스턴스 변수의 상태가 올바른지 서술
  • 메서드에 전달된 파라미터의 값이 올바르게 변경되었는지 서술
  • 반환값이 올바른지 서술

검증이 힘든 경우

  • 한 메서드 안에서 return 문이 여러번 나오는 경우
  • 실행전과 실행후의 값을 비교해야 하는 경우

사후조건을 정의하기위해 제공되는 Contract.Ensures 메서드

 

public Reservation Reserve(Customer customer, int audienceCount)
{
    Contract.Requires(customer != null);
    Contract.Requires(audienceCount >= 1);
    Contract.Ensures(Contract.Result<Reservation>() != null);
    return new Reservation(customer, this, calculateFee(audienceCount), audienceCount);
}

public decimal Buy(Ticket ticket)
{
    Contract.Requires(ticget != null);
    Contract.Ensures(Contract.Result<decimal>() >= 0);
    if(bag.Invited)
    {
        bag.Ticket = ticket;
        return 0;
    }
    else
    {
        bag.Ticket = ticket;
        bag.MinusAmount(tiker.Fee);
        return ticket.Fee;
    }
}

Contract.OldValue<T> 이용하여 메서스 실행전의 text 값에 접근 가능

 

public string Middle(string text)
{
    Contract.Requires(text != null && text.Length >= 2);
    Contract.Ensures(Contract.Result<string>().Length < Contract.OldValue<string>(text).Length);
    text = text.Substring(1, text.Length -2);
    return test.Trim();
}

 

불변식

 

인스턴스 생명주기 전반에 걸쳐 지켜져야 하는 규칙을 명세. 객체의 내부상태와 관련

 

  • 클래스의 모든 인스턴스가 생성된 후에 만족되야 한다. 이것은 클래스에 정의된 모든 생성자는 불변식을 준수해야 한다는 것을 의미.
  • 클라이언트에 의해 호출 가능한 모든 메서드에 의해 준수돼야 한다. 메서드가 실행되는 중에는 객체의 상태가 불안정한 상태로 빠질 수 있기 때문에 불변식을 만족시킬 필요는 없지만 메서드 실행 전과 메서드 종료 후에는 항상 불변식을 만족하는 상태가 유지돼야 함.

원래는 객체의 생성자나 메서드 실행 전후에 불변식을 직접 후출해야 하지만,

Contract.Invariant 메서드를 이용해 불변식을 정의

 

public class Screening
{
    private Movie movie;
    private int sequence;
    private DateTime whenScreened;
    
    [ContractInvariantMethod]
    private void Invariant() {
        Contract.Invariant(movie != null);
        Contract.Invariant(sequence >= 1);
        Contract.Invariant(whenScreened > DateTime.Now);
    }
}

 

 

 


03 계약에 의한 설계와 서브타이핑

 

계약에 의한 설계는 '클라이언트'가 만족시켜야 하는 사전조건과 '클라이언트'의 관점에서 서버가 만족시켜야 하는 사후조건을 기술

 

 서브타입이 리스코프 치환 원칙을 만족시키기 위해서는 클라이언트와 슈퍼타입 간에 체결된 계약을 준수해야 한다.

 

리스코프 치환 원칙의 세분화된 두 종류의 규칙

 

1. 협력에 참여하는 객체에 대한 기대를 표현하는 계약규칙 : 슈퍼타입과 서브타입 사이의 사전조건, 사후조건 불변식에 대한 제약 규칙

  • 서브타입에 더 강력한 사전조건을 정의할 수 없다.
  • 서브타입에 더 완화된 사후조건을 정의할 수 없다.
  • 슈퍼타입의 불변식을 서브타입에서도 반드시 유지돼야 한다.

2. 교체 가능한 타입과 관련된 가변성 규칙 : 파라미터와 리턴타입의 변형과 관련된 규칙

  • 서브타입의 메서드 파라미터는 반공변성을 가져야 한다.
  • 서브타입의 리턴타입은 공변성을 가져야 한다.
  • 서브타입은 슈퍼타입이 발생시키는 예외와 다른 타입의 예외를 발생시켜서는 안 된다

 

 

계약 규칙

 

핸드폰 과금정책을 살펴보자

 

RatePolicy의 서브타입인지? 리스코프 치환원칙을 만족하는지? 

RatePolicy의 구현클래스들이 클라이언트인 Phone과 체결한 계약을 준수하는지 살펴보자

 

public class Phone {
    private RatePolicy ratePolicy;
    private List<Call> calls = new ArrayList<>();
    
    public Phone(RatePolicy ratePolicy) {
        this.ratePolicy = ratePolicy;
    }
    
    public void call(Call call) {
        calls.add(call);
    }
    
    //청구할 요금을 담은 Bill 인스턴스를 생성한 후 반환
    public Bill publishBill() {
        return new Bill(this, reatePolicy.calculateFee(calls));
    }
}


public abstract class BasicRatePolicy implements RatePolicy {
    @Override
    public Money calcuateFee(List<Call> calls) {
        //사전조건
        assert calls != null;
        
        Money result = Money.ZERO;
        
        for(Call call: calls) {
            result.plus(calculateCallFee(call));
        }
        
        //사후조건
        assert result.isGreaterThanOrEqual(Money.ZERO);
        
        return result;
    }
    
    protected abstract Money calculateCallFee(Call call);
}

// 부가정책인 AdditionalRatePolicy 에도 사전,사후조건 추가하자

 

 

 

서브타입에 더 강력한 사전조건을 정의할 수 없다.

서브타입이 슈퍼타입에 정의된 사전조건을 강화하면 기존에 체결된 계약을 위반하게 된다. 슈퍼타입을 대체할 수 없음.

-> 사전조건을 완화시켜서 null 인자를 전달해도 예외가 발생하지 않게 수정. 리스코프 치환원칙을 위반하지 않음.

 

public abstract class BasicRatePolicy implements RatePolicy {
    @Override
    public Money calculateFee(List<Call> calls) {
        if(calls == null) {
            return Money.ZERO;
        }
        ...
    }
}

 

 

서브타입에 더 완화된 사후조건을 정의할 수 없다.

 

RatePolicy의 calculateFee 오퍼레이션의 반환값이 0보다 작은경우

 

calculateFee 오퍼레이션은 반환값이 0보다 커야 한다는 사후조건을 정의

부모클래스인 AdditionalRatePolicy에서 사후조건을 완화시켜보자

-> Bill의 생성자 예외가 발생

-> 사후조건 완화시키는 서버는 클라이언트 관점에서 수용 불가. 

-> 슈퍼타입을 대체할 수 없음. 리스코프 치환원칙 위반.

 

public abstract class AdditionalRatePolicy implements RatePolicy {
    @Override
    public Money calculateFee(List<Call> calls) {
        assert calls != null;
        
        Money fee = next.calculateFee(calls);
        Money result = calculate(fee);
        
        //사후조건
        //assert result.isGreaterThanOrEqual(Money.ZERO);
        
        return result;
    }
    
    abstract protected Money calculate(Money fee);
}


public class Bill {
    public Bill(Phone phone, Money fee) {
        if(fee.isLessThan(Money.ZERO)) {
            throw new IllegalArgumentException();
        }
        ...
    }
}

 

반대로 사후조건을 강화하는 방법은 계약에 아무 문제가 없다.

 

 

슈퍼타입의 불변식을 서브타입에서도 반드시 유지돼야 한다.

 

자식클래스가 계약을 위반할 수 있는 코드를 작성하는 것을 막을 수 있는 유일한 방법은 인스턴스 변수의 가시성을 protected가 아니라 private으로 만드는 것 뿐. 모든 인스턴스 변수의 가시성은 private 으로 제한 되어야 함.

 

가변성 규칙

 

서브타입은 슈퍼타입이 발생시키는 예외와 다른 타입의 예외를 발생시켜서는 안 된다.

- 일반적으로 부모클래스가 던지는 예외가 속한 상속계층이 아닌 다른 상속 계층에 속하는 예외를 던질 경우 자식 클래스는 부모클래스를 대체할 수 없다. 따라서 서브타입이 아님.

 

public interface RatePolicy {
    Money calculateFee(List<Call> calls) throw EmptyCallException;
}

public abstract class AdditionalRatePolicy implements RatePolicy {
    @Override
    public Money calculateFee(List<Call> calls) {
        if(calls == null || calls.isEmpty()) {
            throw new NoneElementException();
        }
        ...
    }
}

public class EmptyCallException extends RuntimeException {...}
// 예외가 상속계층이 같다면 부모클래스 대체 가능. 
public class NoneElementException extends EmptyCallException {...}


public class EmptyCallException extends RuntimeException {...}
// 상속계층이 다른경우, 대체 불가능. 협력의 결과가 벗어낫기 때문
public class NoneElementException extends RuntimeException {...}

 

- 클라이언트 관점에서 부모클래스에 보다 더 적은일을 수행하는 자식클래스는 부모클래스와 동일하지 않다. 서브타입이 아님.

 

서브타입의 리턴타입은 공변성을 가져야 한다.

프로그램의 어떤 위치에서 두 타입 사이의 치환가능성

  • 공변성 covariance : S와 T 사이의 서브타입 관계가 그대로 유지. 이 경우 해당 위치에서 서브타입인 S가 슈퍼타입인 T 대신 사용될 수 있다. 리스코프 치환 원칙은 공변성과 관련된 원칙.
  • 반공변성 contravariance : S와 T 사이의 서브타입 관계가 역전된다. 이 경우 해당 위치에서 슈퍼타입 T가 서브타입 S 대신 사용될 수 있다.
  • 무공변성 invariance :  S와 T 사이에 아무 관계가 존재하지 않음.  둘중 어느 하나도 하나를 대신 사용 불가.
public class Publisher {}

public class IndependentPublisher extends Publisher {}

public class Book {
    private Publisher publisher;
    
    public Book(Publisher publisher) {
        this.publisher = publisher;
    }
}

public class Magazine extends Book {
    public Magazine(Publisher publisher) {
        super(publisher);
    }
}

public class Bookstall {
    public Book sell(IndependentPublisher independentPublisher) {
        return new Book(independentPublisher);
    }
}

public class MagazineStore extends Bookstall {
    @Override
    public Book sell() {
        return new Magazine(independentPublisher);
    }
}

new Customer().order(new BookStall());

new Customer().order(new MagazineStore());

 

슈퍼타입 대신 서브타입을 반환하는 것은 더 강력한 사후조건을 정의하는것과 같다

 

MagazineStore의 sell메서드는 비록 반환타입은 다르지만 Bookstall의 sell메서드를 대체할 수 있다.

-> 부모클래스에서 구현된 메서드를 자식 클래스에서 오버라이딩할 때

부모클래스에서 선언한 반환타입의 서브타입으로 지정할 수 있는 특성 :

리턴타입 공변성 return type covariance - 메서드를 구현한 클래스의 타입계층 방향과 리턴타입의 타입 계층 방향이 동일한 경우

 

언어에 따라 지원 여부가 다름. 자바는 리턴타입 공변성 지원. C#은 리턴타입에 대해 무공변적.

 

서브타입의 메서드 파라미터는 반공변성을 가져야 한다.

-> 파라미터 반공변성은 지원하는 언어는 없으나 제네릭 프로그래밍에서 중요한 의미를 가짐.

 

public class MagazineStore extends BookStall {
// 파라미터를 슈퍼타입으로 변경
    @Override
    public Magazine sell(Publisher publisher) {
        return new Magazine(publisher);
    }
}

 

서브타입 대신 슈퍼타입을 파라미터로 받는것은 더 약한 사후조건을 정의하는 것과 같다.

 

사전조건은 파라미터에 대한 제약조건.

좀 더 완화된 슈퍼타입을 파라미터로 받을 수 있는것.

 

리턴타입은 사후조건과 관련이 있는것.

슈퍼타입에 정의된 사후조건을 강화할 수 있음. 더 강화된 서브타입 인스턴스 반환.

 

 

함수 타입과 서브타이핑

 

이름 없는 메서드 정의

익명함수 anonymous function, 함수 리터럴 function literal, 람다 표현식 lambda expression

 

스칼라의 경우

 

def sell(publisher:IndependentPublisher): Book = new Book(publisher)

 

함수리터럴 표기법 이용 메서드 타입 정의

(publisher: IndependentPublisher) => new Book(publisher)

 

파라미터 타입과 리턴 타입을 이용해 함수타입 정의

IndependentPublisher => Book

 

별도의 클래스 정의하지 않고 메서드로 정의해서 전달

class Customer {

    var book: Book = null

    

    def order(store: IndependentPublisher => Book) : Unit = {

        book = store(new IndependentPublisher())

    }

}

 

Bookstall의 sell메서드를 클래스 정의 없이 함수리터럴을 통해 파라미터로 전달

new Order().order((publisher: IndependentPublisher) => new Book(publisher))

 

파라미터 반공변성을 가지고 리턴타입 공변성을 가질 경우 메서드 오버라이드 가능

new Customer().order((publisher:Publisher) => new Magazine(publisher))

 

파라미터 타입 반공변성 IndependentPublisher                    ->                  Publisher
함수 서브타입 IndependentPublisher =>Book       <-            Publisher=>Magazine
리턴타입 공변성 BookStall                                        <-           MagazineStore