반응형
오브젝트 02 - 객체지향 프로그래밍
2.1 영화 예매 시스템
- 영화, 상영, 할인 정책, 할인 조건이 있는 영화 예매 시스템이 존재한다.
2.2 객체지향 프로그래밍을 향해
2.2.1 협력, 객체, 클래스
진정한 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때에만 얻을 수 있다. 이를 위해서는 아래의 두 가지에 집중해야 한다.
- 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라.
- 클래스는 공통적인 상태와 행동을 공유하는 객체들을 추상화한 것이다.
- 따라서 클래스의 윤곽을 잡기 위해서는 어떤 객체들이 어떤 상태와 행동을 가지는지를 먼저 결정해야 한다.
- 객체를 중심에 두는 접근 방법은 설계를 단순하고 깔끔하게 만든다.
- 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다.
- 객체를 협력하는 공동체의 일원으로 바라보는 것은 설계를 유연하고 확장 가능하게 만든다.
- 객체 지향적으로 생각하고 싶다면 객체를 고립된 존재로 바라보지 말고 협력에 참여하는 협력자로 바라봐야 한다.
- 객체들의 모양과 윤곽이 잡히면 공통된 특성과 상태를 가진 객체들을 타입으로 분류하고 이 타입을 기반으로 클래스를 구현하라.
2.2.2 도메인을 따르는 프로그램 구조
- 소프트웨어는 사용자가 원하는 어떤 문제를 해결하기 위해 만들어진다.
- 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인이라고 부른다.
- 일반적으로 클래스의 이름은 대응되는 도메인 개념의 이름과 동일하거나 유사해야한다.
- 클래스 사이의 관계도 최대한 도메인 개념 관계와 유사하게 만들어 쉽게 이해할수 있어야 한다.
2.2.3 클래스 구현하기
public class Screening {
private Movie movie;
private int sequence;
private LocalDateTime whenScreened;
public Screening(Movie movie, int sequence, LocalDateTime whenScreened) {
this.movie = movie;
this.sequence = sequence;
this.whenScreened = whenScreened;
}
public LocalDateTime getStartTime() {
return whenScreened;
}
public boolean isSequence(int sequence) {
return this.sequence == sequence;
}
public Money getMovieFee() {
return movie.getFee();
}
}
- 인스턴스 변수의 가시성은 private이고 메서드의 가시성은 public라는 점을 주목하자.
- 가장 중요한 것은 클래스의 경계를 구분 짓는 것이다.
- 경계의 명확성이 객체의 자율성을 보장하고 프로그래머에게 구현의 자유를 제공한다.
자율적인 객체
- 객체는 상태와 행동을 함께 가지는 복합적인 존재다.
- 객체는 스스로 판단하고 행동하는 자율적인 존재다.
- 객체는 데이터와 기능을 객체 내부로 함께 묶어 캡슐화를 한다.
- 접근 수정자를 통해 외부에서의 접근을 통제할 수 있는 접근 제어 메커니즘도 제공해 객체를 자율적인 존재로 만든다.
- 퍼블릭 인터페이스 : 외부에서 접근 가능한 부분
- 구현 : 내부에서만 접근 가능한 부분
프로그래머의 자유
- 구현 은닉으로 인해 코드를 사용하는 개발자는 내부 구현은 무시한 채 인터페이스만 알고 있어도 기능을 사용할 수 있다.
- 구현 은닉으로 인해 코드를 작성하는 개발자는 내부 구현을 마음대로 변경할 수 있다.
2.3 할인 요금 구하기
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
this .title = title;
this.runni ngTime = runningTime;
this.fee = fee;
this .discountPolicy = discountPolicy;
}
public Money getFee() {
return fee;
}
public Money calculateMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
- 이 메서드에는 어떤 할인 정책을 사용할 것인지 결정하는 코드가 어디에도 존재하지 않는다.
- 단지 discountPolicy에게 메시지를 전송할 뿐이다.
2.3.1 할인 정책과 할인 조건
- 할인 정책은 금액 할인과 비율할인 두가지로, 대부분의 코드가 유사하다.
- 중복 코드를 제거하기 위해 공통코드를 부모 클래스에 보관 후, 상속받게 한다.
public abstract class DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList<>();
public DiscountPolicy(DiscountCondition ... conditions) {
this.conditions = Arrays.aslist(conditions);
}
public Money calculateDiscountAmount(Screening screening) {
for(DiscountCondition each : conditions) {
if (each.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
abstract protected Money getDiscountAmount(Screening Screening);
}
- DiscountPolicy는 할인 여부와 요금 계산에 필요한 전체적인 흐름은 정의하지만 실제로 요금을 계산 하는 부분은 추상 메서드인 getDiscountAmount 메서드에게 위임한다.
- 실제로는 DiscountPolicy를 상속 받은 자식 클래스에서 오버라이딩한 메서드가 실행될 것이다.
- 이처럼 부모 클래스에 기본적인 알고리 즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴을 TEMPLATE METHOD 패턴이라고 부른다.
2.4 상속과 다형성
2.4.1 컴파일 시간 의존성과 실행 시간 의존성
- 어떤 클래스가 다른 클래스에 접근할 수 있거나 해당 클래스의 객체의 메서드를 호출할 경우 두 클래스는 의존성이 있다고 말한다.
- Movie는 구현 클래스가 아닌 추상 클래스에 의존하고 있다.
- 코드의 의존성과 실행 시점의 의존성은 서로 다를 수 있다.
- 즉, 클래스 사이의 의존성과 객체 사이의 의존성은 동일하지 않을 수 있음
- 장점
- 유연하며 쉽게 재사용 할 수 있음
- 확장 가능
- 디버깅이 쉬움
- 단점
- 코드를 이해하기 어려워짐
- 코드를 이해하기 위해서는 코드뿐만 아니라 객체를 생성하고 연결하는 부분을 찾아야 함
- 디버깅 하기 어려워짐
2.4.2 차이에 의한 프로그래밍
상속
- 클래스의 코드를 수정하지 않고 재사용해서 새로운 클래스를 만들 수 있게 한다.
- 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법
- 클래스 사이의 관계를 설정하는 것만으로 기존 클래스의 모든 속성과 행동을 새 클래스에 물려줄 수 있다.
- 부모 클래스의 구현은 공유하면서도 행동이 다른 자식 클래스를 쉽게 추가할 수 있다.
2.4.3 상속과 인터페이스
- 인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다.
- 상속을 통해 자식 클래스는 자신의 인터페이스에 부모의 인터페이스를 포함하게 된다.
- 결과적으로 자식 클래스는 부모가 수신할 수 있는 모든 메시지를 수신할 수 있기 때문에 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수있다.
public class Movie {
public Money calculateMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
- Movie 객체는 자신과 협력하는 객체가 중요한것이 아니라 calculateDiscountAmount 메시지를 수신하는 것이 중요
- Movie는 협력 객체가 calculateDiscountAmount 라는 메세지를 이해할 수 있다면 그게 어떤 클래스인지는 관심이 없음
- 이처럼 자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅이라고 부른다.
2.4.4 다형성
- 다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미한다.
- Movie는 calculateDiscountAmount 라는 동일한 메세지를 전송하지만, 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스에 달려있다.
- 다형적인 협력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야 한다.
- 다형성은 객체지향 프로그램의 컴파일 시간과 실행 시간의 의존성이 다를 수있다는 사실을 기반으로 한다.
- 메시지와 메서드를 실행 시점에 바인딩 한다 - 지연 바인딩 또는. 동적 바인딩
2.4.5 인터페이스와 다형성
- 구현은 공유할 필요가 없고 순수하게 인터페이스만 공유하고 싶을 때가 있다.
- 자바에서는 인터페이스라는 프로그래밍 요소를 제공한다.
- 추상클래스를 이용한 할인 정책과는 다르게 할인 조건은 구현을 공유할 필요가 없기때문에 인터페이스를 이용해 상속한다.
2.5 추상화와 유연성
2.5.1 추상화의 힘
- 위의 그림은 자식 클래스를 생략한 코드 구조를 그림으로 표현한 것이다.
- 이 그림은 추상화의 두 가지 장점을 보여준다.
- 첫째, 요구사항의 정책을 높은 수준에서 서술 할 수 있다.
- 영화의 예매 요금은 '금액 할인 정책'과 '두개의 순서 조건, 한개의 기간 조건'을 이용해 계산 할 수 있다 -> 영화예매 요금은 최대 하나의 할인정책과 다수의 할인 조건으로 계산 할 수 있다
- 추상화를 사용하면 세부적인 내용을 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다.
- 둘째, 설계가 좀 더유연해진다.
- 추상화를 이용해 상위 정책을 표현하면 기존 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장할 수 있다.
- 첫째, 요구사항의 정책을 높은 수준에서 서술 할 수 있다.
2.5.2 유연한 설계
public class Movie {
public Money calculateMovieFee(Screening screening) {
if (discountPolicy == null) {
return fee;
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
}
- 위 방식의 문제점은 할인이 없을 경우를 예외 케이스로 취급하기 때문에 지금까지의 일관된 협력 방식이 무너진다.
- 기존 경우에는 할인 금액의 계산을 DiscountPolicy에서 했지만, 이 경우에는 Movie에서 하게 되어 책임의 주체가 달라진다.
- 따라서 일관성을 지키기 위해 NoneDiscountPolicy 클래스를 추가하자
public class NoneDiscountPolicy extends DiscountPolicy {
@Override
protected Money getDiscountAmount(Screening screening) {
return Money.ZERO;
}
}
- 중요한 것은 기존의 코드는 수정하지 않고 새로운 클래스(NoneDiscountPolicy)를 추가해 애플리케이션을 확장했다는 것이다.
- 추상화가 유연한 설계를 가능하게 하는 이유는 설계가 구체적인 상황에 결합되는 것을 방지하기 때문이다.
2.5.3 추상 클래스와 인터페이스 트레이드오프
- NoneDiscountPolicy 정책은 getDiscountAmount 메서드를 사용하지 않아, 위처럼 설계를 변경 할 수도 있다.
- 이상적으로는 인터페이스를 사용하도록 변경한 설계가 더 좋으나, 현실적으로는 NoneDiscountPolicy 하나만을 위해 인터페이스를 추가하는 것이 과하다는 생각이 들 수도 있다.
- 구현과 관련된 모든 것들은 트레이드오프의 대상이 될 수 있다.
2.5.4 코드 재사용
- 코드를 재사용하는 방법들 중에는 상속과, 합성이 있다.
- 합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법이다.
- Movie가 DiscountPolicy의 코드를 재사용하는 방법이 바로 합성이다.
- 왜 많은 사람들이 상속 대신 합성을 선호할까?
2.5.6 상속
- 상속은 객체지향에서 코드를 재사용하기 위해 자주 사용된다.
- 하지만 두가지 큰 문제점이 있다.
1. 상속은 캡슐화를 위반한다.
- 상속을 이용하기 위해서는 부모 클래스의 내부 구조를 잘 알고 있어야 한다.
- 부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화가 약화된다.
- 캡슐화 약화는 클래스의 강결합을 만들어 부모 클래스가 변경되면 자식 클래스도 변경될 확률이 높아진다.
- 결과적으로 코드 변경이 어려워진다.
2. 설계를 유연하지 못하게한다.
- 상속은 부모 클래스와 자식 클래스의 관계를 컴파일 시점에 결정한다.
- 실행 시점에 객체의 종류를 변경하는 것이 불가능하다.
2.5.5 합성
- 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 한다.
- Movie는 DiscountPolicy의 인터페이스를 통해 약결합 된다.
- Movie는 DiscountPolicy가 외부에 calcuateDiscountAmount 메서드를 제공한다는 사실만 알고, 내부 구현은 전혀 모른다.
- 합성은 상속이 가지는 두 가지 문제점을 모두 해결한다.
1. 효과적인 캡슐화
- 인터페이스에 정의된 메시지를 통해서만 재사용이 가능해 구현을 효과적으로 캡슐화 할 수 있다.
2. 쉬운 인스턴스 교체
public class Movie {
private DiscountPolicy discountPolicy;
public void changeDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy= discountPolicy;
}
}
Movie avatar = new Movie("아바타", Duration.ofMinutes(120),
Money.wons(10000),
new AmountDiscountPolicy(Money.wons(800), ... ));
avatar.changeDiscountPolicy(new PercentDiscountPolicy(0.1, ... ));
- 간단하게 실행 시점에 할인 정책을 변경할 수 있다.
728x90
반응형
'스터디 > 오브젝트' 카테고리의 다른 글
오브젝트 06 - 메시지와 인터페이스 (0) | 2024.02.26 |
---|---|
오브젝트 05 - 책임 할당하기 (0) | 2024.02.20 |
오브젝트 04 - 설계 품질과 트레이드오프 (0) | 2024.02.19 |
오브젝트 03 - 역할, 책임, 협력 (0) | 2024.02.13 |
오브젝트 01 - 객체, 설계 (0) | 2024.02.06 |
댓글