** 본 글은 오브젝트(조영호) 책을 읽고 작성자가 정리한 글입니다. **
로버트 마틴이 강조하는 소프트웨어 모듈이 가져야하는 3가지 기능 ✅
1. 모듈은 정상적으로 실행되어야 한다.
2. 변경에 용이해야 한다.
3. 이해하기 쉬워야 한다.
객체 지향 설계를 위해 고려할 것 ✅
- 객체 내부는 캡슐화하고 객체 간에 오직 메시지를 통해서만 상호작용 하도록 한다.
- 각 객체가 자율적 존재가 되도록 설계! ( 의존성과 결합성을 낮추자 ) = 책임의 이동
영화 시스템 요구사항 분석🎯
영화 : 영화에 대한 기본정보를 표현
상영 : 실제로 관객들이 영화를 관람하는 사건 (상영 일자, 시간 순번 등 )
** 사용자가 실제로 예매하는 대상은 영화가 아니라 상영 **
** 특정한 조건을 만족하는 예매자는 요금을 할인 받을 수 있음. **
** 할인 조건 : 할인 여부를 결정하며 '순서 조건' 과 '기간 조건'의 두 종류로 구분 **
- 순서조건 :상영 순번을 이용해 할인 여부 결정
- 기간 조건 : 상영 시작 시간을 이용해 할인 여부 결정
- 기간 조건은 요일, 시작 시간, 종료 시간 세 파트로 구성
- Ex. 요일이 월요일, 시작 시간이 오전 10시, 종료 시간이 오후 1시인 기간 조건 사용 시 매주 월요일 오전 10~1시 사이 상영되는 모든 영화에 대해 할인
** 할인 정책 : 할인 요금을 결정하며 '금액 할인 정책'과 '비율 할인 정책' 두 종류로 분류 **
- 금액 할인 정책 : 예매 요금에서 일정 금액 할인
- 비율 할인 정책 : 정가에서 일정 비율의 요금을 할인해 주는 방식
** 영화별로 하나의 할인 정책만 할당할 수 있으며 할인 정책을 지정 하지 않는 것도 가능 **
** 영화별로 여러개의 할인 조건은 가능하며 순번 조건과 기간 조건을 혼합해 사용 가능 **
** 할인을 적용하기 위해서는 할인 조건과 할인 정책을 조합해서 사용 **
** 할인 정책은 1인을 기준으로 측정되기 때문에 예약 인원이 2명이라면 x2 요금할인 **
프로그래밍에 들어가기 전 🙋🏻♀️
1. 어떤 클래스가 필요한지 고민하기 전에 어떤 객체들이 필요한지 고민!
- 클래스는 공통적 상태와 행동을 공유하는 객체들을 추상화 한것! 클래스의 윤곽을 잡기 위해선 어떤 객체들이 어떤 상태와 행동을 갖는 지 먼저 결정
2. 객체를 협력하는 공동체의 일원으로 바라 볼것 -> 설계를 유연하고 확장 가능케 함.
- 공통된 특성과 상태를 가진 객체들의 타입으로 분류하고 타입을 기반으로 클래스를 구현
도메인의 구조를 따르는 프로그램 구조 ✅
도메인 : 문제 해결을 위해 사용자가 프로그램을 사용하는 분야를 의미
- 영화는 여러번 상영될 수 있고 상영은 여러번 예매 될 수 있음
- 영화에는 할인 정책을 할당하더라도 하나만 할당 가능 할인 정책이 존재하는 경우 하나 이상의 할인 조건이 반드시 존재
클래스로 구현하기 ✅
클래스 구현 시 주목할 점!
1. 접근 제어 메커니즘 고려하기
- 접근 수정자 : public, protected, private
- 객체 내부에 대한 접근을 통제할 것 : 객체를 자율적 존재로 만든다
- 인스턴스 변수의 가시성은 private, 메서드의 가시성은 public을 고려! ( 내외부 공개 여부를 신중히 결정할 것 )
- 객체의 상태는 숨기고 행동만 공개
2. 캡슐화
- 데이터와 기능을 객체 내부로 함께 묶는 것을 의미
- 인터페이스와 구현의 분리가 객체 지향 프로그래밍의 핵심
package Domain;
import java.time.LocalDateTime;
public class Screening { /* 상태와 행동을 함께 가지는 복합적 객체*/
private Movie movie; // 인스턴스 변수의 가시성 private
private int sequence;
private LocalDateTime whenScreened;
public Screening(Movie movie, int sequence, LocalDateTime whenScreened){
this.movie = movie;
this.sequence = sequence;
this.whenScreened = whenScreened;
}
public Reservation reserve(Customer customer, int audienceCount){
return new Reservation(customer, this, calculateFee(audienceCount), audienceCount);
}
private Money calculateFee(int audienceCount){
return movie.calculateMovieFee(this).times(audienceCount);
}
public LocalDateTime getStartTime(){
return whenScreened;
}
public boolean isSequence(int sequence){ //public 메소드를 통해서만 내외부 상태 체크,변경
return this.sequence == sequence;
}
public Money getMovieFee(){
return movie.getFee();
}
}
- 협력하는 객체들의 공동체 : reserve 메서드에서는 calculateFee라는 메서드를 호출해 요금 계산 후 reservation생성자에 전달
package Domain;
import java.math.BigDecimal;
public class Money { // 저장하는 값이 금액과 관련되었음을 나타내고자 long타입이 아닌 money 객체 사용
public static final Money ZERO = Money.wons(0);
private final BigDecimal amount;
Money(BigDecimal amount){
this.amount = amount;
}
public static Money wons(long amount){
return new Money(BigDecimal.valueOf(amount));
}
public static Money wons(double amount){
return new Money(BigDecimal.valueOf(amount));
}
public Money plus(Money amount){
return new Money(this.amount.add(amount.amount));
}
public Money minus(Money amount){
return new Money(this.amount.subtract(amount.amount));
}
public Money times(double percent){
return new Money(this.amount.multiply(BigDecimal.valueOf(percent)));
}
public boolean isLessThan(Money other){
return amount.compareTo(other.amount) < 0;
}
public boolean isGreaterThanOrEqual(Money other){
return amount.compareTo(other.amount) >=0;
}
}
package Domain;
public class Reservation {
private Customer customer;
private Screening screening;
private Money fee;
private int audienceCount;
public Reservation(Customer customer, Screening screening, Money fee, int audienceCount){
this.customer = customer;
this.screening = screening;
this.fee = fee;
this.audienceCount = audienceCount;
}
}
- 협력하는 객체들의 공동체 : 영화를 예매하기 위해 Screening, Movie, Reservation 인스턴스들이 서로의 메서드를 호출하며 상호작용
할인 요금 구하기 ✅
package Domain;
import java.time.Duration;
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.runningTime = runningTime;
this.fee = fee;
this.discountPolicy = discountPolicy;
}
public Money getFee() {
return fee;
}
/**
* Movie는 기본 요금인 Fee 에서 반환된 할인 요금을 차감.
* @param screening
* @return 기본요금-할인된 요금
*/
public Money calculateMovieFee(Screening screening){
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
public void changeDiscountPolicy(DiscountPolicy discountPolicy){
this.discountPolicy = discountPolicy;
}
}
- 할인 정책의 종류를 판단할 수 있어야 하고 이는 discountPolicy를 통해! ( movie는 어떤 정책인 지 크게 신경쓰지 않고 메세지만 전달 )
package Domain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* AmountDiscountPolicy , PercentDiscountPolicy 가 존재
* 두 클래스는 대부분 코드가 중복되기때문에 부모 클래스인 DiscountPolicy안에 장복 코드를 두고 상속받아 사용
* DiscountPolicy의 인스턴스를 생성할 필요가 없기 때문에 추상 클래스로 구현
*/
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;
}
/**
* 부모클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에 위임하는 디자인 패턴
* - > templage Method
* @param screening
* @return
*/
abstract protected Money getDiscountAmount(Screening screening);
}
package Domain;
/**
* DiscountPolicy의 자식클래스로서 할인 조건을 만족할 경우 일정한 금액을 할인해 주는 금액 할인 정책
*/
public class AmountDiscountPolicy extends DiscountPolicy {
private Money discountAmount;
public AmountDiscountPolicy(Money discountAmount, DiscountCondition ... conditions){
super(conditions);
this.discountAmount = discountAmount;
}
@Override
protected Money getDiscountAmount(Screening screening){
return discountAmount;
}
}
package Domain;
public class PercentDiscountPolicy extends DiscountPolicy{
private double percent;
public PercentDiscountPolicy(double percent, DiscountCondition ...conditions){
super(conditions);
this.percent = percent;
}
@Override
protected Money getDiscountAmount(Screening screening){
return screening.getMovieFee().times(percent);
}
}
- 부모 클래스인 DiscountPolicy 안에 중복 코드를 두고 AmountDiscountPolicy와 PercentDiscountPolicy가 이를 상속받아 사용
- DiscountPolicy의 경우 인스턴스를 생성할 필요가 없기 때문에 추상 클래스로 구현
- 하나의 할인 정책은 여러개의 할인 조건을 포함할 수 있으며 해당 값을 리스트로 저장하고 있음
- DiscountPolicy는 할인 여부와 요금 계산에 필요한 전체적 흐름을 정의하지만 실제 처리는 자식 클래스가 수행 : Template 패턴
- 자식 클래스인 AmountDiscountPolicy와 PercentDiscountPolicy에서 실제 할인 요금 계산 함수 오버라이딩!
package Domain;
public interface DiscountCondition { // 인터페이스
boolean isSatisfiedBy(Screening screening);
}
package Domain;
public class SequenceCondition implements DiscountCondition { // 인터페이스 상속
private int sequence;
public SequenceCondition(int sequence){
this.sequence = sequence;
}
/**
* 상영 순서와 전달된 파라미터의 순번이 일치할 경우 할인 가능한 것으로 판단.
* @param screening
* @return
*/
public boolean isSatisfiedBy(Screening screening){
return screening.isSequence(sequence);
}
}
package Domain;
import java.time.DayOfWeek;
import java.time.LocalTime;
public class PeriodCondition implements DiscountCondition{
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public PeriodCondition(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
this.dayOfWeek = dayOfWeek;
this.startTime = startTime;
this.endTime = endTime;
}
@Override
public boolean isSatisfiedBy(Screening screening) {
return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
startTime.compareTo(screening.getStartTime().toLocalTime()) <=0 &&
endTime.compareTo(screening.getStartTime().toLocalTime()) >=0;
}
}
할인 정책 정리 ✅
package Domain;
import java.time.Duration;
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private DiscountPolicy discountPolicy;
/**
* movie 는 discountpolicy에 의존한다.
* 그러나 실행 시점에는 movie의 인스턴스는 amountpolicy 나 PercentDiscountPolicy의 인스턴스에 의존하게 된다.
* = 코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있음.
*/
public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy){
this.title = title;
this.runningTime = runningTime;
this.fee = fee;
this.discountPolicy = discountPolicy;
}
public Money getFee() {
return fee;
}
}
- 하나의 영화에 대해 단 하나의 할인 정책만 설정할 수 있지만! 할인 조건의 경우 여러개 설정할 수 있도록 클래스가 잘 구성됨!!
Movie avartar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new AmountDiscountPolicy(Money.wons(800),
new SequenceCondition(1),
new SequenceCondition(10),
new PeriodCondition(DayOfWeek.MONDAY, LocalTime.of(10,0), LocalTime.of(11,59)),
new PeriodCondition(DayOfWeek.THURSDAY, LocalTime.of(10, 0), LocalTime.of(20,59))));
컴파일 시간 의존성 VS 실행 시간 의존성 🐳
코드의 의존성과 실행 시점의 의존성이 다르면 다를 수록 코드는 이해하기 어려워지나 유연성과 확장성은 증가
위 예제에서 Movie 코드는 DiscountPolicy에 의존하고 있지만 실행 시에는 AmountDiscountPolicy 또는 PercentDiscountPolicy에 의존하게 된다!
설계가 유연해질 수록 코드를 이해하고 디버깅하기는 점점 더 어려워 진다.
하지만 유연성을 억제하면 재사용서과 확장 가능성은 낮아진다.
상속과 다형성 정리 🐳
상속은 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법
상속을 이용하면 클래스 사이에 관계를 설정하는 것만으로 기존 클래스가 가지고 있는 모든 속성과 행동을 새로운 클래스에 포함시킬 수 있음!
DiscountPolicy에 정의된 모든 속성과 메서드를 그대로 물려받는 AmountDiscountPolicy와 PercentDiscountPolicy 클래스는 상속의 강력함을 잘 보여주는 예시!
상속을 이용하면 부모 클래스의 구현은 공유하면서도 행동이 다른 자식 클래스를 쉽게 추가할 수 있다.
부모클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍이라고 부름
+ 구현 상속과 인터페이스 상속
구현 상속 : 서브 클래싱 -> 코드를 재사용하기 위한 목적
인터페이스 상속 : 서브 타이핑 -> 다형적 협력을 위해 부모 클래스와 자식 클래스가 인터페이스를 공유할 수 있도록 상속을 이용하는 것
* 상속은 되도록 인터페이스 상속을 위해 사용되어야 한다. *
다형성 : 어떤 객체에서 동일한 메세지를 전송하지만 실제로 어떤 메세드가 실행 될 것인지는 메세지를 수신하는 객체 클래스에 따라 달라지는 것!
위 예제에서 보면, moive는 DiscountPolicy 클래스에게 메시지를 전송하지만 실행 시점에 실제로 실행되는 메서드는 Movie와 협력하는 객체의 실제 클래스에 따라 달라짐!
동일한 메세지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미
다형성을 구현하는 방법은 매우 다양하지만 메세지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 실행시점에 결정된다는 공통점에 기반! -> 동적 바인딩
인터페이스와 다형성
DiscountPolicy를 추상 클래스로 구현함으로써 자식 클래스들이 인터페이스와 내부 구현을 함께 상속 받도록 만들었다.
추상 클래스를 이용해 다형성을 구현했던 할인 정책과 달리 할인 조건은 구현을 공유할 필요가 없기 때문에 자바의 인터페이스를 이용해 타입 계층을 구현
- 요구사항의 정책을 높은 수준에서 서술 할 수 있음 ( 영화 예매 요금은 최대 하나의 할인 정책과 다수 할인 조건을 이용해 계산 가능 )
- 추상화를 이용하면 설계가 좀 더 유연해 짐
- 기본적 애플리케이션의 협력 흐름을 기술 할 수 있음 ( Movie -> DiscountPolicy -> DiscountCondition을 향해 흐름)
* 재사용 가능한 설계의 기본을 이루는 디자인 패턴이나 프레임워크 모두 추상화를 이용해 상위 정책을 정의 *
코드 재사용 측면에서의 상속 VS 합성 🐳
상속은 객체 지향에서 코드 재사용을 위해 널리 사용되자만 설계에 좋지 않은 영향을 미침
1. 캡슐화를 위반하게 됨 : 상속 이용을 위해 부모 클래스의 내부 구조를 알아야 함 ( 부모 클래스의 구현이 자식에게 노출 )
2. 설계를 유연하지 못하게 함 : 과도한 상속이용 시 코드 변경이 어려움
위 예시를 보면! 실행 시점에 객체 종류 변경이 어렵다!
실행시점에 금액 할인 정책인 영화를 비율 할인 정책으로 변경할 경우, 상속을 이용한 설계에서는 AmountDiscountMovie의 인스턴스를 PercentDiscountMoive인스턴스로 변경해야함. -> 부모와 자식 클래스가 강하게 결합 되어있기 때문
이에 상속 보다는 합성을 선호!
합성 : 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법
movie가 discountpolicy의 코드를 재사용하는 방법이 바로 합성
기존 인스턴스 변수로 연결한 방법에서 실행 시점에 할인 정책을 변경하는 방법 ( 합성 예시 )
package Domain;
import java.time.Duration;
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private DiscountPolicy discountPolicy;
public void changeDiscountPolicy(DiscountPolicy discountPolicy){
this.discountPolicy = discountPolicy;
}
}
/**
* avatar.changeDiscountPolicy(new PercentDiscountPolicy(0.1, ...));
*/
상속 보다는 인스턴스 변수로 관계를 연결한 원래의 설계가 더 유연
movie는 요금을 계산하기 위해 DiscountPolicy의 코드를 재사용
상속과 다른 점은 상속이 부모 클래스의 코드와 자식 클래스의 코드를 컴파일 시점에 하나의 단위로 강하게 결합하는 데 비해 Movie가 DiscountPolicy의 인터페이스를 통해 약하게 결합 된다는 것
즉 합성을 사용하면,
1. 상속보다 구현을 효과적으로 캡슐화가 가능
2. 의존하는 인스턴스를 교체하는 것이 비교적 쉬워 설계가 유연해짐
코드를 재사용하는 경우에는 상속보다 합성을 선호하는 것이 옳지만 다형성을 위해 인터페이스를 재사용하는 경우 상속과 합성을 조합해 사용
본 예제에서는 Movie와 DiscountPolicy는 합성 관계로 연결되어 있고 Discountpolicy와 amountdiscountpolicy, percentdiscountpolicy는 상속관계로 연결 되어있음.
[ 참고 도서 ]
https://wikibook.co.kr/object/
오브젝트: 코드로 이해하는 객체지향 설계
역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라! 객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두 번째 걸음은 객체를
wikibook.co.kr
'데일리IT🌱' 카테고리의 다른 글
디자인 패턴과 프로그래밍 패러다임_싱글톤 패턴 (0) | 2023.01.04 |
---|---|
오브젝트 _ 코드로 이해하는 객체지향 설계( 3장 ) (0) | 2022.12.09 |
TDD ( Test- Driven - Development ) 에 대하여 (0) | 2022.10.20 |
Git Flow & MR & Code Review 효율적 사용 _ 기술블로그 보는! (1) | 2022.10.10 |
[ 운영체제 ] 인터럽트 & 시스템콜 (0) | 2022.03.23 |
댓글