www.notion.so/4-abaa24321532499abe2ccf1ac0821531

 

4. 책임주도 설계의 대안

책임주도설계는 익숙해지기위해서 부단한 노력과 시간이 필요하다.

www.notion.so

  • 책임주도설계는 익숙해지기위해서 부단한 노력과 시간이 필요하다.
  • ( 매 우 어 렵 다 )
  • 이럴때는 최대한 빠르게 목적한 기능을 수행하는 코드를 작성하는 것이다.
  • 일단 코드상에 명확하게 드러나는 책임들을 보고 협력과 책임에 관해 고민하면서 책임들을 올바른 위치로 옮기는 것이다.
  • 주로 객체지향 설계에 대한 경험이 부족한 개발자들과 페어 프로그래밍을 할때나 설계의 실마리가 풀리지 않을때 이런 방법을 사용하는데 생각보다 훌륭한 설계를 얻게 되는 경우가 종종 있다.
  • 주의할 점은 코드를 수정 한 후에 겉으로 드러나는 동작이 바뀌어서는 안된다는 것이다.
  • 캡슐화를 향상 시키고 응집도를 높이고 결합도를 낮춰야 하지만 동작은 그대로 유지해야 한다.
  • 이처럼 이해하기 쉽고 수정하기 쉬운 소프트웨어로 개산하기위해 겉으로 보이는 동작은 바꾸지 않은채 내부 구조를 변경하는 것을 리팩토링 이라고 부른다.

메서드 응집도

  • 데이터 중심으로 설계된 ReservationAgency에 포함된 로직들을 적절한 객체의 책임으로 분배하면 책임주도설계와 거의 유사한 결과를 얻을 수 있다.

public class ReservationAgency { public Reservation reserve(Screening screening, Customer customer, int audienceCount) { Movie movie = screening.getMovie(); boolean discountable = false; for(DiscountCondition condition : movie.getDiscountConditions()) { if (condition.getType() == DiscountConditionType.PERIOD) { discountable = screening.getWhenScreened().getDayOfWeek().equals(condition.getDayOfWeek()) && condition.getStartTime().compareTo(screening.getWhenScreened().toLocalTime()) <= 0 && condition.getEndTime().compareTo(screening.getWhenScreened().toLocalTime()) >= 0; } else { discountable = condition.getSequence() == screening.getSequence(); } if (discountable) { break; } } Money fee; if (discountable) { Money discountAmount = Money.ZERO; switch(movie.getMovieType()) { case AMOUNT_DISCOUNT: discountAmount = movie.getDiscountAmount(); break; case PERCENT_DISCOUNT: discountAmount = movie.getFee().times(movie.getDiscountPercent()); break; case NONE_DISCOUNT: discountAmount = Money.ZERO; break; } fee = movie.getFee().minus(discountAmount).times(audienceCount); } else { fee = movie.getFee().times(audienceCount); } return new Reservation(customer, screening, fee, audienceCount); } }

  • 단점
    1. 어떤일을 수행하는지 한눈에 파악하기 어렵기 때문에 코드를 전체적으로 이해하는데 너무 많은 시간이 걸린다.
    2. 하나의 메서드 안에서 너무 많은 작업을 처리하기 때문에 변경이 필요할 때 수정해야 할 부분을 찾기 어렵다.
    3. 메소드 내부의 일부 로직만 수정하더라도 메소드의 나머지 부분에서 버그가 발생할 확률이 높다.
    4. 로직의 일부만 재사용하는 것이 불가능하다.
    5. 코드를 재사용하는 유일한 방법은 원하는 코드를 복사해서 붙여넣는것 뿐이므로 코드 중복을 초래하기 쉽다.
  • 한마디로 말해서 응집도가 낮아서 이해하기도 어렵고 재사용하기도 어려우며 변경하기도 어렵다 이러한 메소드를 몬스터 메소드라고 부른다.
  • 우선 메소드를 작게 분리해서 메소드의 응집도를 높여라.
  • 응집도가 높은 메소드는 변경되는 이유가 단 하나여야 한다.
  • 클래스가 작고 목적이 명확한 메소드들로 구성돼 있다면 변경을 처리하기 위해 어떤 메소드를 수정해야 하는지를 쉽게 판단할 수 있다.
  • 메소드의 크기가 작고 목적이 분명하기 떄문에 재사용 하기도 쉽다.
  • 위와 같은 이유로 메소드를 짧게 만들고 이해하기 쉬운 이름으로된 메소드로 만들자.

public class ReservationAgency { public Reservation reserve(Screening screening, Customer customer, int audienceCount) { boolean discountable = checkDiscountable(screening); Money fee = calculateFee(screening, discountable, audienceCount); return createReservation(screening, customer, audienceCount, fee); } private boolean checkDiscountable(Screening screening) { return screening.getMovie().getDiscountConditions().stream() .anyMatch(condition -> condition.isDiscountable(screening)); } private Money calculateFee(Screening screening, boolean discountable, int audienceCount) { if (discountable) { return screening.getMovie().getFee() .minus(calculateDiscountedFee(screening.getMovie())) .times(audienceCount); } return screening.getMovie().getFee(); } private Money calculateDiscountedFee(Movie movie) { switch(movie.getMovieType()) { case AMOUNT_DISCOUNT: return calculateAmountDiscountedFee(movie); case PERCENT_DISCOUNT: return calculatePercentDiscountedFee(movie); case NONE_DISCOUNT: return calculateNoneDiscountedFee(movie); } throw new IllegalArgumentException(); } private Money calculateAmountDiscountedFee(Movie movie) { return movie.getDiscountAmount(); } private Money calculatePercentDiscountedFee(Movie movie) { return movie.getFee().times(movie.getDiscountPercent()); } private Money calculateNoneDiscountedFee(Movie movie) { return movie.getFee(); } private Reservation createReservation(Screening screening, Customer customer, int audienceCount, Money fee) { return new Reservation(customer, screening, fee, audienceCount); } }

  • 전체적인 흐름을 이해하기가 쉬워진다.
  • 각 메서드는 단 하나의 이유에 의해서만 변경된다.
  • 메서드들의 응집도 자체는 높아졌지만 클래스의 응집도는 여전히 낮다.
  • 클래스의 응집도를 높이기 위해서는 메소드들의 책임을 적절한 위치로 이동시켜야 한다.

객체를 자율적으로 만들자.

  • 객체를 자율적으로 만든다는것은 자신이 소유하고 있는 데이터를 자기 스스로 처리하도록 만드는것이 자율적인 객체를 만드는 지름길이다.
  • 따라서 메소드가 사용하는 데이터를 저장하고 있는 클래스로 메소드를 이동시키면 된다.
  • 일반적으로 메소드를 다른클래스로 이동시킬때는 인자에 정의된 클래스 중 하나로 이동하는 경우가 일반적이다.
  • 이렇게 책임을 옮긴후에 Polymorphism 패턴과 Protected Variations 패턴 을 사용하면 아까와 유사한 객체지향설계로 설계된 구조가 나온다.

www.notion.so/3-f8ee4fbf26be40a6938d3cbf72992a8b

3. 구현을 통한 검증

  • Screening을 구현
    • 예제링크
    • https://github.com/abcdsds/object/tree/master/chapter04
    • 이렇게 책임이 정해졌으니 책임에 필요한 인스턴스 변수를 결정.
    • 상영시간 및 상영 순번을 인스턴스 변수로 포함
    • Moive에 가격을 계산하라는 메시지를 전송해야 하기 때문에 Moive에 대한 참조도 포함

public class Screening { private Movie movie; private int sequence; private LocalDateTime 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 getWhenScreened() { return whenScreened; } public int getSequence() { return sequence; } }

  • calculateFee는 수신자인 moive가 아니라 송신자인 Screening의 의도를 표현한것
  • moive에 내부구현에 대한 어떤 지식도 없이 전송할 메시지를 결정했다는 것이다.
  • 이렇게 movie의 구현을 고려하지 않고 필요한 메시지를 결정하면 Moive의 내부 구현을 깔끔하게 캡슐화 할수 있다.
  • Moive와 Screening의 연결관계는 메시지 뿐이다. 따라서 moive의 내부구현에 어떠한 수정을 가하더라도 Screening에는 영향을 미치지 않는다.
  • 메시지를 기반으로 협력을 구성하면 Screening와 Moive 사이의 결합도를 느슨하게 유지할수있다.
  • Movie를 구현
    • Screening에서 보낸 메시지를 응답하는 메서드를 구현해야 한다.public class Movie { private String title; private Duration runningTime; private Money fee; private MovieType movieType; private Money discountAmount; private double discountPercent; private List<PeriodCondition> periodConditions; private List<SequenceCondition> sequenceConditions; public Movie(String title, Duration runningTime, Money fee, List<PeriodCondition> periodConditions, List<SequenceCondition> sequenceConditions) { this.title = title; this.runningTime = runningTime; this.fee = fee; this.periodConditions = periodConditions; this.sequenceConditions = sequenceConditions; } public Money calculateMovieFee(Screening screening) { if (isDiscountable(screening)) { return fee.minus(calculateDiscountAmount()); } return fee; } private boolean isDiscountable(Screening screening) { return checkPeriodConditions(screening) || checkSequenceConditions(screening); } private boolean checkPeriodConditions(Screening screening) { return periodConditions.stream() .anyMatch(condition -> condition.isSatisfiedBy(screening)); } private boolean checkSequenceConditions(Screening screening) { return sequenceConditions.stream() .anyMatch(condition -> condition.isSatisfiedBy(screening)); } private Money calculateDiscountAmount() { switch(movieType) { case AMOUNT_DISCOUNT: return calculateAmountDiscountAmount(); case PERCENT_DISCOUNT: return calculatePercentDiscountAmount(); case NONE_DISCOUNT: return calculateNoneDiscountAmount(); } throw new IllegalStateException(); } private Money calculateAmountDiscountAmount() { return discountAmount; } private Money calculatePercentDiscountAmount() { return fee.times(discountPercent); } private Money calculateNoneDiscountAmount() { return Money.ZERO; } }
    public enum MovieType { AMOUNT_DISCOUNT, // 금액 할인 정책 PERCENT_DISCOUNT, // 비율 할인 정책 NONE_DISCOUNT // 미적용 } public class DiscountCondition { private DiscountConditionType type; private int sequence; private DayOfWeek dayOfWeek; private LocalTime startTime; private LocalTime endTime; public boolean isSatisfiedBy(Screening screening) { if (type == DiscountConditionType.PERIOD) { return isSatisfiedByPeriod(screening); } return isSatisfiedBySequence(screening); } private boolean isSatisfiedByPeriod(Screening screening) { return dayOfWeek.equals(screening.getWhenScreened().getDayOfWeek()) && startTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0 && endTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0; } private boolean isSatisfiedBySequence(Screening screening) { return sequence == screening.getSequence(); } }

DiscountCondition 개선하기

  • 변경에 취약한 클래스를 포함하고 있다.
  • 그것은 바로 DiscountCondition이다. 메시지를 보내는것이아닌 값을 받아와서 처리하고 있기 때문이다.
  • 3가지 이유로 변경될수 있다.
    1. 새로운 할인 조건 추가
      • isSatisfiedBy 메소드 안의 if else 구문을 수정해야 한다.
      • 새로운 할인조건이 새로운 데이터를 요구한다면 DiscountCondition에 속성을 추가하는 작업도 필요
    2. 순번 조건을 판단하는 로직
      • isSatisfiedBySequence 메소드의 내부 구현을 수정해야한다.
      • 순번조건을 판단하는데 필요한 데이터가 변경된다면 DIscountCondition의 Sequence 속성 역시 변경해야 할것이다.
    3. 기간 조건을 판단하는 로직이 변경되는 경우
      • isSatisfiedByPeriod 메소드의 내부 구현을 수정해야 한다.
      • 기간조건을 판단하는데 필요한 데이터가 변경되면 DiscountCondition의 속성도 변경해야하기 떄문이다.
    • DiscountCondition은 하나 이상의 변경 이유를 가지기 때문에 응집도가 낮다.
    • 응집도가 낮다는것은 서로 연관성이 없는 기능이나 데이터가 하나의 클래스안에 뭉쳐져있다는것을 의미한다.
    • 낮은 응집도가 유발하는 문제를 해결하기 위해서는 변경의 이유에 따라 클래스를 분리해야 한다
    • 코드를 통해 변경의 이유를 파악할 수 있는 첫번째 방법은 인스턴스 변수가 초기화 되는 시점 을 살펴보는 것이다
    • 응집도가 높은 클래스는 인스턴스를 생성할때 모든 속성이 함께 초기화 된다.
    • 반면 응집도가 낮은 클래스는 객체의 속성 중 일부만 초기화하고 일부는 초기화 되지 않은 상태로 남겨진다.
    • DiscountCondition을 살펴보면 조건에 따라 특정 인스턴스변수만 초기화된다.
    • 클래스의 속성이 서로 다른시점에 초기화되거나 일부만 초기화된다는 것은 응집도가 낮다는 증거다.
    • 따라서 함께 초기화되는 속성을 기준으로 코드를 분리해야 한다
    • 코드를 통해 변경의 이유를 파악할수 있는 두번째 방법은 메서드들이 인스턴스 변수를 사용하는 방식 을 살펴보는 것이다.
    • 모든 메서드가 객체의 모든 속성을 사용한다면 클래스의 응집도는 높다고 볼수 있다.
    • 반면 메소드를 사용하는 속성에 따라 그룹이 나뉜다면 클래스의 응집도가 낮다고 볼수 있다.
    • 특정메서드는 특정 필드만 사용한다
    • 이럴경우 응집도를 높이기 위해서 속성 그룹과 해당 그룹에 접근하는 메소드 그룹을 기준으로 코드를 분리해야 한다
    클래스 응집도 판단하기
    • 클래스가 하나 이상의 이유로 변경돼야 한다면 응집도가 낮은것이다. 변경의 이유를 기준으로 클래스를 분리하라.
    • 클래스의 인스턴스를 초기화하는 시점에 경우에 따라 서로 다른 속성들을 초기화하고 있다면 응집도가 낮은것이다. 초기화되는 속성의 그룹을 기준으로 클래스를 분리하라.
    • 메서드 그룹이 속성 그룹을 사용하는지 여부로 나뉜다면 응집도가 낮은것이다. 이들 그룹을 기준으로 클래스를 분리하라.

타입 분리하기

  • DiscountCount는 순번 조건과 기간조건이라는 두개의 독립적인 타입이 하나의 클래스안에 공존하고 있다는 것이다.
  • 두 타입을 SequenceCondition과 PeriodCondition이라는 두개의 클래스로 분리하자.

public class PeriodCondition { 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; } public boolean isSatisfiedBy(Screening screening) { return dayOfWeek.equals(screening.getWhenScreened().getDayOfWeek()) && startTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0 && endTime.compareTo(screening.getWhenScreened().toLocalTime()) >= 0; } } public class SequenceCondition { private int sequence; public SequenceCondition(int sequence) { this.sequence = sequence; } public boolean isSatisfiedBy(Screening screening) { return sequence == screening.getSequence(); } }

  • 분리하면 위에 언급한 문제들이 모두 사라진다.
  • 하지만 문제가 발생 Moive와 협력하는 클래스는 DiscountCondition 하나뿐이였는데 두개로 늘어나버렸다.
  • 첫번째로 해결하는 방법은 두가지 목록을 각각 필드로 가지고 있는것이다.
  • 하지만 이문제는 DiscountCondition을 구현한 클래스 양쪽 모두에게 결합된다는 것이다.
  • 클래스를 분리한 후에 설계의 관점에서 결합도가 높아졌다.
  • 또한 새로운 할인조건을 추가하기가 더 어려워졌다는 것이다.
  • 할인조건을 추가하려면 새로운 조건리스트를 또 추가해야된다.
  • 응집도는 높아졌지만 변경과 캡슐화라는 관점에서 보면 전체적으로 설계의 품질이 나빠졌다.

다형성을 통해 분리하기

  • Moive의 입장에선 SequenceCondition이나 PeriodCondition은 아무 차이가 없다.
  • 이때 역할 이라는 개념이 등장한다.
  • Moive의 입장에서 SequenceCondition과 PeriodCondition이 동일한 책임을 수행한다는 것은 동일한 역할을 수행한다는것을 의미한다.
  • 역할은 협력안에서 대체 가능성을 의미
  • 역할을 구현하기 위해 추상클래스나 인터페이스를 사용하고 Moive에는 그 인터페이스만 바라보게해서 역할이 어떤건지만 알게하자.

public interface DiscountCondition { boolean isSatisfiedBy(Screening screening); } 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; } public boolean isSatisfiedBy(Screening screening) { return dayOfWeek.equals(screening.getWhenScreened().getDayOfWeek()) && startTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0&& endTime.compareTo(screening.getWhenScreened().toLocalTime()) >= 0; } } public class SequenceCondition implements DiscountCondition { private int sequence; public SequenceCondition(int sequence) { this.sequence = sequence; } public boolean isSatisfiedBy(Screening screening) { return sequence == screening.getSequence(); } } public class Movie { private String title; private Duration runningTime; private Money fee; private List<DiscountCondition> discountConditions; private MovieType movieType; private Money discountAmount; private double discountPercent; public Money calculateMovieFee(Screening screening) { if (isDiscountable(screening)) { return fee.minus(calculateDiscountAmount()); } return fee; } private boolean isDiscountable(Screening screening) { return discountConditions.stream() .anyMatch(condition -> condition.isSatisfiedBy(screening)); } }

  • Moive가 가진 discountConditions에 인스턴스가 sequenceCondition이라면 이 클래스의 메소드를 실행할 것이고 periodCondition이라면 이 클래스의 메소드를 실행할 것이다.
  • Moive와 DiscountCondition 사이의 협력은 다형적이다
  • DiscountCondition에서 알수 있듯이 객체의 암시적인 타입에 따라 행동을 분기해야한다면 암시적인 타입을 명시적인 클래스로 정의하고 행동을 나눔으로써 응집도를 해결할수 있다.
  • 다시 말해 객체의 타입에 따라 변하는 행동이 있다면 타입을 분리하고 변화하는 행동을 각 타입의 책임으로 할당하라는 것이다.
  • 이를 GRASP에서는 POLYMORPHISM ( 다형성 ) 패턴이라고 한다.

 

그림출처 https://songii00.github.io/2020/01/27/2020-01-27-OBJECTS Item 02/

다형성 패턴

  • 조건에 따른 변화 if else 또는 switch case등의 조건논리를 사용해서 설계한다면 새로운 조건이 추가될 경우 논리도 수정해야한다.
  • 이것은 변경에 취약해진다.
  • 하지만 다형성 패턴은 조건적인 논리를 사용하지말라고 경고하는대신 다형성을 사용해 새로운 변화를 다루기 쉽게 확장하라고 권고한다.

변경으로 부터 보호하기.

  • 위의 구조에서 DiscountCondition이 새로운 할인조건을 추가한다고 했을경우
  • DiscountCOndition이 Moive로 부터 Sequence와 period의 존재를 감춘다
  • 결국 Moive는 새로운 할인조건이 추가되더라도 영향을 받지 않는다.
  • 이처럼 변경을 캡슐화하도록 책임을 할당하는것을 GRASP에서는 PROTECTED VARIATIONS ( 변경 보호 ) 라고 한다.

Protected Variations 패턴

  • 변화가 예상되는 불안정한 지점들을 식별하고 그 주위에 안정된 인터페이스를 형성하도록 책임을 할당하라.
  • Protected Variations패턴은 책임 할당의 관점에서 캡슐화를 설명한 것이다.
  • 우리가 캡슐화해야하는것은 변경이다. 변경이 될 가능성이 높은가? 그렇다면 캡슐화하라.
  • 하나의 클래스가 여러 타입의 행동을 구현하고 있는것 같다면 ? 클래스를 분해하고 다형성패턴에 따라 책임을 분산시키자.
  • 예측 가능한 변경으로 인해 여러 클래스들이 불안정해진다면 Protected Variations 패턴에 따라 안정적인 인터페이스 뒤로 변경을 캡슐화하라.
  • 두 패턴을 조합하면 변경과 확정에 유연하게 대처할수 있는 설계를 얻을것이다.

Moive 클래스 개선하기

  • Moive도 할인정책으로 여러 타입의 행동을 구현하고있다
  • 위와 같이 다형성 패턴을 이용해 분리하자.
  • 그리고 Protected Variations 패턴을 이용해 타입의 종류를 안정적인 인터페이스 뒤로 캡슐화할수 있다는것을 의미한다.

public abstract class Movie { private String title; private Duration runningTime; private Money fee; private List<DiscountCondition> discountConditions; public Movie(String title, Duration runningTime, Money fee, DiscountCondition... discountConditions) { this.title = title; this.runningTime = runningTime; this.fee = fee; this.discountConditions = Arrays.asList(discountConditions); } public Money calculateMovieFee(Screening screening) { if (isDiscountable(screening)) { return fee.minus(calculateDiscountAmount()); } return fee; } private boolean isDiscountable(Screening screening) { return discountConditions.stream() .anyMatch(condition -> condition.isSatisfiedBy(screening)); } protected Money getFee() { return fee; } abstract protected Money calculateDiscountAmount(); } public class AmountDiscountMovie extends Movie { private Money discountAmount; public AmountDiscountMovie(String title, Duration runningTime, Money fee, Money discountAmount, DiscountCondition... discountConditions) { super(title, runningTime, fee, discountConditions); this.discountAmount = discountAmount; } @Override protected Money calculateDiscountAmount() { return discountAmount; } } public class PercentDiscountMovie extends Movie { private double percent; public PercentDiscountMovie(String title, Duration runningTime, Money fee, double percent, DiscountCondition... discountConditions) { super(title, runningTime, fee, discountConditions); this.percent = percent; } @Override protected Money calculateDiscountAmount() { return getFee().times(percent); } }

  • 이제 클래스의 모든 내부 구현을 끝냈다.
  • 모든 내부구현은 캡슐화 돼있고 모든 클래스는 변경의 이유를 오직 하나씩만 가진다.
  • 각 클래스는 응집도가 높고 다른 클래스와 최대한 느슨하게 결합돼 있다.
  • 객체지향설계의 기본은 책임과 협력에 초점을 맞추는 것이다.

도메인 구조가 코드의 구조를 이끈다.

  • 변경 역시 도메인모델의 일부다.
  • 도메인 모델에는 도메인 안에서 변하는 개념과 이들 사이의 관계가 투영돼 있어야 한다.
  • 도메인모델에는 할인정책과 할인조건이 변경될수 있다는 도메인에 대한 직관이 반영돼있다.
  • 이 직관이 우리가 설계를 가져야 하는 유연성을 이끌었다.
  • 강조하지만 구현을 가이드할 수 있는 도메인 모델을 선택하라. 객체지향은 도메인의 개념과 구조를 반영한 코드를 가능하게 만들기 때문에 도메인의 구조가 코드의 구조를 이끌어내는 것은 자연스러울뿐만 아니라 바람직한 것이다.

변경과 유연성

  • 설계를 주도하는 것은 변경이다.
  • 변경에 대비하는 두가지 방법은 하나는 코드를 이해하고 수정하기 쉽도록 최대한 단순하게 설계하는 것이다.
  • 다른 하나는 코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 더 유연하게 만드는 것이다.
  • 유사한 변경이 반복적으로 발생하고 있다면 복잡성이 상승하더라도 유연성을 추가하는 두번째 방법이 더 좋다.
  • 예를 들어 영화에 설정된 할인 정책을 실행중에 변경할수 있어야 한다는 요구사항이 추가 됬다고 하면
  • 현재 설계에서는 할인정책을 구현하기 위해 상속을 사용하고 있기떄문에 실행중에 영화의 할인정책을 변경하기위해서는 새로운 인스턴스를 생성한후 필요한 정보를 복사해야한다.
  • 또한 변경 전후의 인스턴스가 개념적으로는 동일한 객체를 가리키지만 물리적으로는 서로 다른 객체이기 때문에 식별자의 관점에서 혼란스러울 수 있다.
  • 런타임시에 변경하기에 좋은 방법은 아니다 이럴때 합성이 좋은 해결방안이 된다.
  • Moive의 상속 계층안에 구현된 할인정책을 DiscountPolicy로 독립적으로 분리한후 Moive에 합성시키면 유연한 설계가 완성된다.
  • 합성을 사용하면 간단하게 Moive에 연결된 DiscountPolicy 인스턴스를 교체하면 끝이다.
  • 이 예는 유연성에 대한 압박이 설계에 어떤 영향을 미치는지를 잘 보여준다.
  • 실제로 유연성은 의존성 관리의 문제다.
  • 유연성의 정도에 따라 결합도를 조절할수 있는 능력은 객체지향 개발자가 갖춰야하는 중요한 기술중 하나다.

코드의 구조가 도메인의 구조에 대한 새로운 통찰력을 제공한다.

  • 코드의 구조가 바뀌면 도메인에 대한 관점도 함께 바뀐다.
  • 도메인 모델은 도메인에 포함된 개념과 관계뿐만 아니라 도메인이 요구하는 유연성도 정확하게 반영해야한다.
  • 도메인 모델은 단순히 도메인의 개념과 관계를 모아놓은 것이 아니다.
  • 도메인 모델은 구현과 밀접한 관계를 맺어야 한다.
  • 도메인 모델은 코드에 대한 가이드를 제공할 수 있어야 하며 코드의 변화에 발맞춰 함께 변화해야한다.
  • 도메인 모델을 코드와 분리된 막연한 무엇으로 생각하지 말자.

www.notion.so/2-GRASP-c485daef91e74ae1925641406f126b7c

2. 책임 할당을 위한 GRASP 패턴

  • General Responsibility Assignment Software Pattern (일반적인 책임할당을 위한 소프트웨어 패턴)

도메인 개념에서 출발하기

  • 설계 시작전 도메인에 대한 개략적인 모습을 그려보는것이 유용하다.
  • 어떤 책임을 할당할때 가장 먼저 고민해야하는 유력한 후보가 바로 도메인

 

  • 설계를 시작할때 완벽할 필요는 없다.
  • 도메인 개념을 정리하는데 너무 많은 시간을 들이지말고 빠르게 설계와 구현을 진행하라.
  • 올바른 도메인 모델이란 존재하지 않는다
  • 실용적이면서 유용한 모델이 정답이다

정보 전문가에게 책임을 할당하라

  • 책임을 애플리케이션에 대해 전송된 메시지로 간주하고 이 메시지를 책임질 첫번째 객체를 선택하는 것으로 설계를 시작한다.
  • 책임을 수행하는데 필요한 메시지를 결정해야 한다.
  • 메시지는 메시지를 수신할 객체가 아니라 메시지를 전송할 객체의 의도를 반영해서 결정해야 한다.

메시지를 전송할 객체는 무엇을 원하는가 ?

  • 우선 첫번째는 바로 영화를 예매하는 것이다.
  • 메시지의 이름은 예매하라가 절절한것 같다.

메시지를 수신할 적합한 객체는 누구인가 ?

  • 객체가 상태와 행동을 통합한 캡슐화의 단위 이므로 스스로 처리해야하는 자율적인 존재여야만 한다.
  • 객체의 책임과 책임을 수행하는데 필요한 상태는 동일한 객체 안에 존재해야 한다.
  • 따라서 객체에게 책임을 할당하는 첫 번째 원칙은 책임을 수행할 정보를 알고 있는 객체에게 책임을 할당하는 것이다.
  • GRASP에서는 이를 INFORMATION EXPERT (정보 전문가) 라고 한다.
  • 여기서 이야기하는 정보는 데이터와 다르다
  • 책임을 수행하는 객체가 정보를 알고 있다고 해서 그 정보를 저장하고 있을 필요는 없다.
  • 객체는 해당 정보를 제공 할수 있는 다른 객체를 알고 있거나 필요한 정보를 계산해서 제공할 수도 있다.
  • 어떤 방식이건 정보 전문가가 데이터를 반드시 저장하고 있을 필요는 없다.
  • 이제 예매하라 메시지를 수신했을 때 Screening이 수행해야 하는 작업의 흐름을 생각해보자, 이제부터는 외부의 인터페이스가 아닌 Screening의 내부로 들어가 메시지를 처리하기 위해 필요한 절차와 구현을 고민해보는 것이다.
  • 지금은 개략적인 수준에서 객체들의 책임을 결정하는 단계이기 때문에 너무 세세한 부분까지 고민할 필요는 없다.
  • 단지 Screening이 책임을 수행하는 데 필요한 작업을 구상해보고 스스로 처리할 수 없는 작업이 무엇인지를 가릴 정도의 수준이면 된다.
  • 만약 스스로 처리할수 없다면 외부에 도움을 요청해야 한다. 이요청이 외부로 전송해야하는 새로운 메시지가 되고, 최종적으로 이 메시지가 새로운 객체의 책임으로 할당된다. 이같은 연쇄적인 메시지 전송과 수신을 통해 협력 공동체가 구성되는 것이다.

예매에 필요한 것은 영화의 가격이다 Screening은 스스로 처리할수 없고 이 정보는 Movie가 들고 있다.

  • movie는 영화가격을 계산할 책임을 가지게 됬다.

Movie는 스스로 할인여부를 판단할수 없다. 메시지를 전송해 외부에 요청하자.

  • DiscountCondition이 이 책임을 할당받을수 있다.
  • 이 친구는 스스로 처리할수 있기에 외부에 요청하지 않는다.
  • 이렇게 Information Expert 패턴은 객체에게 책임을 할당할 때 가장 기본이 되는 책임할당 원칙이다.
  • Information Expert 패턴은 객체란 상태와 행동을 함께 가지는 단위라는 객체지향의 가장 기본적인 원리를 책임할당의 관점에서 표현한다.
  • Information Expert패턴을 따르는것만으로도 자율성 높은 객체들로 구성된 협력 공동체를 구축할 가능성이 높아지는것이다.

높은 응집도와 낮은 결합도

  • 설계는 트레이드오프 란것을 기억하자.
  • 올바른 책임 할당을 위해 Information Expert 패턴 이외의 다른 책임 할당 패턴들을 함께 고려할 필요가 있다.
  • 위에서 보면 Moive가 DiscountCondition에 할인여부를 묻는데 이건 Screening에서도 물어봐도된다. 하지만 왜 Moive가 물어보게 했을까 ?
  • 그 이유는 응집도와 결합도에 있다
  • 책임을 할당할 때 항상 고려해야되는 기본 원리다.
  • 다양한 대안들이 존재한다면 응집도와 결합도의 측면에서 더 나은 대안을 선택하는 것이 좋다.
  • 다시말해 두 협력 패턴중에서 높은 응집도와 낮은 결합도를 얻을수 있는 설계가 있다면 그것을 선택해야 한다는 것이다.
  • GRASP에서는 이를 low Coupling ( 낮은 결합도 ) 패턴과 High Cohesion ( 높은 응집도 ) 패턴 이라고 부른다.
  • 도메인을 보면 Moive는 이미 DiscounCondition을 가지고 있다 낮은 결합도를 위해 Moive에서 하는것이 더 좋다.
  • 이를 높은 응집도 측면에서 본다면 Screening의 책임은 예매하는것이다.
  • 하지만 요금계산과 관련된 책임을 일부 떠안을 경우 요금계산식이 변경되면 Screening이 변경되고 서로 다른 이유로 변경되는 책임을 짊어지게 된다. 그러므로 응집도가 낮아질수 밖에 없다

창조자에게 객체 생성 책임을 할당하라.

  • 영화예매 협력의 최종 결과물은 Reservation 인스턴스를 생성하는 것이다.
  • 이것은 어떤 객체에게 Reservation 인스턴스를 생성할 책임을 할당해야 한다는 것을 의미한다.
  • GRASP의 CREATOR ( 창조자 ) 패턴은 이같은 경우에 어떤 객체에게 할당할지 지침을 제공하는 패턴이다.
    1. B가 A객체를 포함하거나 참조한다.
    2. B가 A객체를 기록한다.
    3. B가 A객체를 긴밀하게 사용한다.
    4. B가 A객체를 초기화하는데 필요한 데이터를 가지고 있다 ( 이 경우 B는 A에 대한 정보전문가다 )
    • CREATOR 패턴의 의도는 그 객체를 사용해야 하는 객체는 어떤 방식으로든 생성될 객체와 연결될 것이기 때문에 잘알고 있어야한다.
    • 다시 말해 두 객체는 서로 결합한다.
    • 이미 결합돼 있는 객체에게 생성 책임을 할당하는 것은 추가적인 결합도가 생성되지 않으므로 결합도에 영향을 끼치지 않는다.
  • Screening이 Moive도 알고 예매정보를 생성하는 것에 대한 정보도 정보전문가이므로 Screening이 CREATOR로 적당하다

 

  • 책임에 초점을 맞춰서 설계할 때 직면하는 가장 큰 어려움은 어떤 객체에게 어떤 책임을 할당할지를 결정하기가 쉽지 않다는 것이다.
  • 책임 할당과정은 일종의 트레이드오프다.
  • 동일한 문제를 해결할수 있는 다양한 책임 할당 방법이 존재하며 어떤 방법이 최선인지는 상황과 문맥에 따라 달라진다.
  • GRASP 패턴은 책임 할당의 어려움을 해결하기 위한 답을 제시해 줄 것이다.
  • GRASP 패턴을 배우면 응집도와 결합도 캡슐화 같은 다양한 기준에 따라 책임을 할당하고 결과를 트레이드오프할 수 있는 기준을 배우게 될것이다.

 

1. 책임주도설계를 향해

  • 데이터보다 행동을 먼저 결정하라
  • 협력이라는 문맥 안에서 책임을 결정하라

데이터보다 행동을 먼저 결정하라

  • 객체에게 중요한것은 데이터가 아니라 외부에 제공하는 행동이다
  • 데이터 → 행동으로 객체의 초점을 변경한다.
  • 데이터중심설계와 달리 책임주도설계는 이 객체가 수행해야할 책임은 무엇인가 ?
  • 를 결정 한 후에 이 책임을 수행해야하는데 필요한 데이터는 무엇인가 를 결정한다.
  • 즉 책임을 먼저 결정하고 데이터를 결정한다.
  • 객체의 책임을 어떻게 적절하게 할당하냐는 협력에서 그 실마리를 찾을 수 있다

협력이라는 문맥안에서 책임을 결정하라

  • 객체에게 할당된 책임의 품질은 협력에 적합한 정도로 결정된다.
  • 객체에 책임된 할당이 협력에 어울리지 않으면 그 책임은 나쁜 것이다
  • 메시지를 전송하는 클라이언트의 의도에 적합한 책임을 할당해야 한다.
  • 메시지가 클라이언트의 의도를 표현한다

이 클래스가 필요한 것은 알겠는데 클래스는 무엇을 해야하지 ? 가 아닌

메시지를 전송해야하는지 누구에게 전송해야하지 ? 라고 질문해야 한다.

  • 객체를 결정하기 전에 객체가 수신할 메시지를 먼저 결정한다는 점 역시 주목.
  • 클라이언트는 어떤 객체 메시지를 수신할지 알지 못한다.
  • 클라이언트는 단지 임의의 객체가 메시지를 수신할것이라는 사실을 믿고 자신의 의도를 표현한 메시지를 전송할 뿐이다.
  • 그리고 메시지를 수신하기로 결정된 객체는 메시지를 처리할 책임 을 할당받게 된다.
  • 메시지를 먼저 결정하기 떄문에 송신자는 수신자에 대한 어떠한 가정도 할 수 없다.
  • 캡슐화가 깔끔하게 이루어진다.
  • 올바른 객체지향 설계는 클라이언트가 전송할 메시지를 결정한 후에야 비로소 객체의 상태를 저장하는데 필요한 내부 데이터에 관해 고민하기 시작한다.

책임 주도 설계

  • 시스템이 사용자에게 제공해야하는 기능인 시스템 책임을 파악한다.
  • 시스템 책임을 좀더 작은 책임으로 분할한다.
  • 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
  • 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
  • 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 된다.
  • 객체지향 설계의 핵심은 협력, 책임, 역할이다.
  • 협력은 애플리케이션의 기능을 구현하게 위해 메시지를 주고받는 객체들 사이의 상호작용
  • 책임은 객체가 다른 객체와 협력하기 위해 수행하는 행동
  • 역할은 대체 가능한 책임의 집합이다.

1. 데이터 중심의 영화 예매 시스템

  • 객체지향 설계에서는 두가지 방법을 이용해 시스템을 객체로ㅓ 분할할수 있다.
    1. 첫번째 방법은 상태를 분할의 중심축으로 삼는 방법이고
    2. 두번째 방법은 책임을 분할의 중심축으로 삼는 방법이다.
  • 데이터 중심 관점
    1. 객체는 자신이 포함하고 있는 데이터를 조작하는데 필요한 오퍼레이션을 정의한다.
    2. 객체의 상태에 초점
    3. 객체를 독립된 데이터 덩어리로 봄
  • 책임 중심 관점
    1. 객체는 다른 객체가 요청할 수 있는 오퍼레이션을 위해 필요한 상태를 보관한다.
    2. 객체의 행동에 초점
    3. 객체를 협력하는 공동체의 일원으로 봄
  • 훌륭한 객체지향 설계는 책임 에 중심을 둬야 한다.
  • 왜 why ?
    • 객체의 상태는 구현에 속한다. 구현은 언제든지 변경된다.
    • 상태를 객체분할의 중심축으로 삼으면 구현에 대한 세부사항이 객체의 인터페이스에 스며들게되서 캡슐화가 깨진다.
    • 상태변경은 인터페이스의 변경을 초래하며 인터페이스에 의존하는 모든 객체에게 변경의 영향이 끼친다.
  • 하지만 객체를 중심에 두면 ?
    • 인터페이스로 캡슐화해서 구현변경에 대한 파장이 외부로 퍼지는것을 막는다.

데이터를 준비하자.

2. 설계 트레이드 오프

  • 데이터 중심설계와 책임 중심설계의 장단점을 비교하기 위해 캡슐화, 응집도, 결합도 를 사용

캡슐화

  • 상태와 행동을 하나의 객체안에 감추는것은 외부로부터 내부구현을 감추기 위한것.
    • 여기서 구현은 나중에 변경될 로직
    • 내부효과를 감추면 파급력을 외부로 전파하지 않아도 된다.
    • 외부로 노출시키는것은 상대적으로 안정적인 부분 ( 변경이 잦지 않은 것 ) 만 노출
    • 변경될 가능성이 높은 부분을 구현이라고 부른다
    • 상대적으로 안정적인 부분을 인터페이스라고 부른다.
    • 객체를 설계하기위한 가장 기본적인 아이디어는 변경의 정도에 따라 구현과 인터페이스를 분리하고 외부에서는 인터페이스에만 의존하도록 관계를 조절하는 것이다.
  • 객체지향에서 가장 중요한 원리는 캡슐화 다.
  • 캡슐화는 외부에서 알 필요가 없는 부분을 감춤으로써 대상을 단순화하는 추상화의 한 종류다.
  • 객체지향설계에서 가장 중요한원리는 안정적인 인터페이스를 외부로 내세우고 세부구현은 인터페이스 뒤로 캡슐화 하는것.
  • 정리하면 캡슐화는 변경 가능성이 높은것을 객체 내부로 숨기는 추상화 기법.
  • 어떤것을 캡슐화 해야 하는가 ? 변경될 가능성이 있는 모든 것.

응집도와 결합도

응집도

  • 응집도는 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다.
  • 모듈내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 그 모듈은 높은 응집도를 가진다.
  • 만약 모듈들이 각각 다른목적을 추구한다면 낮은 응집도를 가진다.
  • 응집도는 객체지향에서는 얼마나 관련높은 책임들을 할당했는지를 나타낸다.

결합도

  • 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도다.
  • 어떤 모듈이 다른 모듈에 너무 자세한 부분까지 알고 있다면 두 모듈은 높은 결합도를 가진다.
  • 어떤 모듈이 다른 모듈에대해 꼭 필요한 지식만 알고있다면 두 모듈은 낮은 결합도를 가진다.
  • 객체지향의 관점에서는 얼마나 협력에 필요한 적절한 수준의 관계만을 유지하고 있는지를 나타낸다.

의문점

  • 응집도는 어느정도가 높은것이고 낮은것인가
  • 결합도는 어느정도가 높은것이고 낮은것인가

  • 높은 응집도와 낮은 결합도를 가진 설계는 설계가 변경하기 쉽다.
  • 변경의 관점에서 응집도란 변경이 발생할때 모듈 내부에서 발생하는 변경의 정도
  • 하나의 변경을 위해 모듈전체가 같이 변경되면 ?
    • 높은 응집도
  • 하나의 변경에 일부모듈만 변경되면 ?
    • 낮은 응집도
  • 하나의 변경에 대해 하나의 모듈만 변경된다면 응집도는 높지만 다소의 모듈이 함께 변경해야 한다면 응집도가 낮은것이다.
  • 결합도는 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도
  • 다시 말해 하나의 모듈을 수정할때 얼마나 많은 모듈을 함께 수정해야 하는지를 나타낸다.
  • 결합도가 높으면 높을수록 변경해야되는 모듈의 수가 늘어나기 때문에 변경하기가 어려워진다.
  • 내부 구현을 변경했을때 다른 모듈도 변경해야 하는것은 높은 결합도
  • 퍼블릭 인터페이스만을 수정했을때 모듈들을 변경해야 하는것은 낮은 결합도
  • 결합도가 높아도 상관없는 경우
    • 변경될일이 거의없는 안정적인 모듈에 의존하는것은 문제가 되지 않는다.
    • 표준 라이브러리나 성숙 단게에 접어든 프레임워크
    • 예를 들어 자바의 String, ArrayList를 들수 있다.

3. 데이터 중심의 영화예매 시스템의 문제점

  • 데이터 중심의 영화예매 시스템처럼 객체 내부의 접근자와 수정자에 과도하게 의존하는 설계방식을 추측에 의한 설계 전략 이라고 부른다
  • 위와 같은 전략을 사용하면 캡슐화를 위반하고 변경에 취약한 설계를 얻게된다.

높은 결합도

  • 데이터 중심 설계는 객채 내부의 구현이 외부 나타나고 내부 구현 변경시 외부에도 그 여파가 퍼져나간다는것이다.

낮은 응집도

  • 서로 다른 이유로 변경되는 코드가 하나의 모듈 안에 공존할 때 모듈의 응집도가 낮다고 말한다.
  • 낮은 응집도는 여러 모듈을 뭉쳐놓았기 때문에 변경과 아무 상관없는 코드들이 영향을 받게된다.
    • A와 B는 아무 관련이 없는데 A가 수정되서 문제가 발생하면 B도 안돌아가기 때문
  • 그렇기 떄문에 하나의 요구사항을 변경하기 위해 동시에 여러 모듈을 수정해야한다.
  • 응집도가 낮을경우 다른 모듈에 위치해야할 책임이 엉뚱하게 위치해있기 때문이다.

4. 자율적인 객체를 향해

캡슐화를 지켜라

  • 캡슐화는 설계의 제1원리다.
  • 데이터중심의 예매가 몸살을 앓는건 캡슐화를 위반했기 때문이다.
  • 객체는 스스로의 상태를 책임져야하며 외부에서는 인터페이스에 정의된 메서드를 통해서만 상태에 접근할 수 있어야 한다.
  • 접근권한자를 private으로 설정했다고 하더라도 게터세터를 쓰면 public으로 열어논것과 다름없다.

스스로 자신의 데이터를 책임지는 객체

  • 상태와 행동을 객체라는 하나의 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리할 수 있게하기 위해서다.
  • 객체는 단순히 데이터 제공자가 아닌 객체 내부에 저장되는 데이터보다 객체가 협력에 참여하면서 수행할 책임을 정의하는 오퍼레이션이 더 중요하다.
  • 데이터 중심 설계에서 객체를 구성하는 질문 두가지 ( 이렇게 하면 안된다 )
    1. 이 객체가 어떤 데이터를 포함해야 하는가 ?
    2. 이 객체가 데이터에 대해 수행해야하는 오퍼레이션은 무엇인가 ?

5. 하지만 여전히 부족하다.

  • 설계가 조금 나아졌긴 했지만 여전히 첫번째 설계에서 발생했던 대부분의 문제는 두번째 설계에서도 여전히 발생한다.

캡슐화 위반

  • 수정된 객체는 분명 자신의 데이터를 스스로 처리한다.
  • 내부 구현의 변경이 외부로 퍼져나가는 파급효과 는 캡슐화가 부족하다는 명백한 증거다.
  • 캡슐화의 진정한 의미는
    • 단순히 객체 내부의 데이터를 외부로 감추는 것 이상으로 단순히 내부속성을 외부로 감추는것은 데이터 캡슐화일 뿐이다.
    • 캡슐화란 변할수 있는 어떤것이라도 감추는것이다 그게 무엇이든 구현과 관련된것이라면.
    • 그것이 속성의 타입이건 할인정책의 종류건 상관없이 내부구현의 변경으로 인해 외부의 객체가 영향을 받는다면 캡슐화를 위반한것이다.
    • 설계에서 변하는것이 무엇인지 고려하고 변하는 개념을 캡슐화 해야 한다.

높은 결합도

  • 캡슐화 위반으로 B객체 에 대한 내부 구현이 외부로 노출됬기 때문에 객체와 B객체의 결합도는 높을수 밖에 없다.
  • 결합도가 높으면 변경에 영향을 많이 받는다.
  • 캡슐화가 위반되서 그렇다 캡슐화가 제 1 원칙이다.

낮은 응집도

  • 마찬가지로 캡슐화를 위반했기 때문이다.

6. 데이터 중심 설계의 문제점

  • 3,4,5를 거쳐 변경해도 설계가 변경에 유연하지 못한 이유는 캡슐화를 위반해서다.
  • 데이터 중심 설계의 문제점 두가지
    1. 데이터 중심의 설계는 본질적으로 너무 이른시기에 데이터에 관해 결정하도록 강요한다.
    2. 데이터중심의 설계에서는 협력이라는 문맥을 고려하지않고 객체를 고립시킨채 오퍼레이션을 결정한다.

데이터 중심 설계는 객체의 행동보다 상태에 초점을 맞춘다.

  • 데이터 중심 설계에서 객체를 구성하는 질문 두가지 (4단락 마지막) 을 보면
  • 데이터가 무엇인가부터 시작한다 이것은 처음부터 데이터를 너무 이른시기부터 초점을 맞추게 한다.
  • 그렇게되면 객체는 단순 데이터 집합이고 캡슐화가 무너지게 된다.

데이터 중심 설계는 객체를 고립시킨 채 오퍼레이션을 정의하도록 만든다.

  • 객체지향 애플리케이션을 구현한다는 것은 협력하는 객체들의 공동체를 구축한다는 것을 의미한다.
  • 따라서 협력이라는 문맥 안에서 필요한 책임을 결정하고 이를 수행할 적절한 객체를 결정하는 것이 가장 중요하다.
  • 캡슐화를 위반해서 인터페이스에 구현이 노출되어 있었기 때문에 다른 결합된 객체들도 영향을 받는다.

'book > 오브젝트' 카테고리의 다른 글

5장 - 2. 책임 할당을 위한 GRASP 패턴  (0) 2021.05.09
5장 - 1 책임 할당하기  (0) 2021.05.08
오브젝트 3장 - 역할, 책임, 협력  (0) 2021.04.30
오브젝트 2장  (0) 2021.04.29
오브젝트 1장  (0) 2021.04.28
  • 다형성은 지연 바인딩 이라는 매커니즘 ( 왜냐면 런타임시에 결정되기 때문에 )
  • 상속은 코드를 재사용하게하는 널리알려진 방법이지만 캡슐화를 뭉갠다.
  • 캡슐화 측면에서는 합성이 더 좋다.
  • 유연한 객체지향 프로그램을 위해서는 컴파일 시간 의존성과 실행 시간 ( 런타임 ) 의존성이 달라야 한다.
  • 객체지향 패러다임의 핵심 역할 , 책임 , 협력

1. 협력

  • 애플리케이션이 기능을 구현하기 위해 수행하는 상호작용을 협력
  • 객체가 협력에 참여하기 위해 수행하는 로직은 책임
  • 객체들이 협력안에서 수행하는 책임들이 모여 객체가 수행하는 역할을 구성한다.
  • 메세지 전송 ( message sending) 은 객체 사이의 협력을 위해 사용할 수 있는 유일한 커뮤니케이션 수단이다.
  • 메시지를 수신한 객체는 메서드를 실행해 요청에 응답한다.
  • 객체를 자율적으로 만드는 가장 기본적인 방법은 내부 구현을 캡슐화 하는 것이다.
  • 객체가 자신의 정보를 바탕으로 직접 계산하면 결합도를 느슨하게 유지할 수 있다.
    • 다른 클래스의 변경의 여파도 줄일수 있다.

책임

  • 협력에 필요한 행동들을 할수있는 적절한 객체를 찾는것이다. 이때 협력에 참여하기 위해 객체가 수행하는 행동을 책임 이라고 부른다.
  • 책임 이란 객체에 의해 정의되는 응집도 있는 행위의 집합
    • 주로 책임은 무엇을 알고 있는가 ? 와 무엇을 할수 있는가로 구성된다.
    • 크레이그 라만은 이러한 분류 체계에 따라 객체의 책임을 크게 하는것과 아는것 두가지 범주로 나누어 세분화하고 있다.
    • 하는것
      • 객체를 생성하거나 계산을 수행하는 등의 스스로 하는것
      • 다른 객체의 행동을 시작시키는 것
      • 다른 객체의 활동을 제어하고 조절하는 것
    • 아는 것
      • 사적인 정보에 관해 아는것
      • 관련된 객체에 관해 아는것
      • 자신이 유도하거나 계산할 수있는 것에 관해 아는것.
  • 책임은 객체가 수행할수 있는 행동을 종합적이고 간략하게 서술하기 때문에 메세지보다 추상적이고 개념적으로도 메세지보다 더 크다.
  • 처음에는 단순한 책임이라고 생각했던것이 여러개의 메시지로 분할되기도 하고 하나의 객체가 수행할 수있다고 생각했던 책임이 여러객체들이 협력해야만 하는 커다란 책임으로 자라는 것이 일반적이다.

책임할당

  • 자율적인 객체를 만드는 가장 기본적인 방법은 그 정보를 갖고있는 객체에게 책임을 할당하는 것이다.
  • 이를 책임할당을 위한 Information Expert 정보 전문가 패턴 이라고한다.
  • 협력을 설계하는 출발점은 시스템이 사용자에게 제공하는 기능을 시스템이 담당할 하나의 책임으로 바라보는 것이다.
  • 객체지향 설계는 시스템의 책임을 완료하는데 필요한 더 작은 책임을 찾아내고 이르 객체들에게 할당하는 반복적인 과정을 통해 모양을 갖춰간다.
  • 항상 전문가에게만 요청하는 것은 아니다 어떤 경우엔 응집도와 결합도의 관점에서 정보전문가가 아닌 다른 객체에 책임을 할당하는것이 더 적절한 경우도 있다.

책임주도 설계

  • 협력을 설계하기 위해서는 책임에 초점을 맞춰야 한다는것.
  • 책임주도설계
    1. 시스템이 사용자에게 제공하는 기능인 시스템 책임을 파악한다.
    2. 시스템 책임을 더 작은 책임으로 분할한다.
    3. 분할된 책임을 수행할수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
    4. 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
    5. 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 된다.

메시지가 객체를 결정한다.

  • 객체에게 책임을 할당하는 데 필요한 메시지를 먼저 식별하고 메시지를 처리할 객체를 나중에 선택했다는것이 중요하다.
  • 다시 말해 객체가 메시지를 선택하는것이 아니라 메시지가 객체를 선택하게 했다.
  • 메시지가 객체를 선택해야 하는 이유
    1. 객체가 최소한의 인터페이스를 가질수 있게 된다.
    2. 객체는 충분히 추상적인 인터페이스 를 가질수 있게 된다
    • 객체의 인터페이스는 필요한 메시지가 식별될때까지 객체의 퍼블릭 인터페이스에 어떤것도 추가하지 않기 때문에 적당한 크기의 퍼블릭 인터페이스를 가질 수 있다.
    • 객체의 인터페이스는 무엇을 하는지는 표현해야 하지만 어떻게 하는지를 노출해서는 안된다. 메시지는 외부의 객체가 요청하는 무언가를 의미하기 때문에 메시지를 먼저 식별하면 무엇을 수행할지에 초점을 맞추는 인터페이스를 얻을 수 있다.

행동이 상태를 결정한다.

  • 객체가 존재하는 이유는 협력에 참여하기 위해서다.
  • 따라서 객체는 협력에 필요한 행동을 제공해야하며 객체를 객체답게 만드는 것은 객체의 상태가아니라 객체가 다른 객체에게 제공하는 행동
  • 초보자들은 먼저 객체에 필요한 상태가 무엇인지를 결정하고 그 후에 상태에 필요한 행동을 결정한다.
  • 이런 방식은 객체 내부 구현이 객체의 퍼블릭 인터페이스에 노출되도록 만들기 때문에 캡슐화를 저해한다.
  • 객체 내부의 구현을 변경하면 인터페이스도 함께 변경되고 결국 객체에 의존하는 클라이언트로 변경의 영향이 전파된다.
  • 객체 내부구현에 초점을 맞춘 설계 방법을 데이터 주도 설계라고 부른다.
  • 캡슐화를 위반하지 않도록 구현에 대한 결정을 뒤로 미루면서 객체의 행위를 고려하기 위해서는 항상 협력이라는 문맥안에서 객체를 생각해야 한다
  • 객체의 상태가 아니라 행동이 중요하다
  • 행동이 그객체의 책임이 된다

역할

역할과 협력

  • 객체는 협력이라는 주어진 문맥 안에서 특정한 목적을 가진다.
  • 객체의 목적은 협력 안에서 객체가 맡게되는 책임의 집합으로 표시된다.
  • 이처럼 객체가 어떤 특정한 협력안에서 수행하는 책임의 집합을 역할 이라고 한다.
  • 책에서 들어주는 예가 역할을 수행하는 객체를 선택하는게 인상적임.

유연하고 재사용 가능한 협력

  • 역할이 중요한 이유는 역할을 통해 유연하고 재사용 가능한 협력을 얻을 수 있기 때문이다
  • 역할은 다른것으로 교체 할 수 있는 책임의 집합이다
  • 역할을 구현하는 가장 일반적인 방법은 추상 클래스와 인터페이스를 사용하는 것이다

객체 대 역할

  • 객체는 동일한 종류의 객체가 하나의 역할을 항상 수행한다면 둘은 동일한 것이다.
  • 하지만 어떤 협력에서 하나 이상의 객체가 동일한 책임을 수행할 수 있다면 역할은 서로 다른 방법으로 실행할 수 있는 책임의 집합이 된다.
  • 역할이란 프로그램이 실행될 때 소프트웨어기계 장치에서 적절한 객체로 메워 넣을 수 있는 하나의 슬롯으로 생각 할수 있다.
  • 대부분의 경우에 어떤 것이 역할이고 어떤 것이 객체인지 또렷하게 드러나지는 않는다.
  • 저자 개인적인 견해로는 설계 초반에는 적절한 책임과 협력의 큰 그림을 탐색하는것이 가장 최우선 목표이고 역할과 객체를 명확하게 구분하는것은 그렇게 중요하지는 않다는 것이다.
  • 따라서 애매하다면 단순하게 객체로 시작하고 반복적으로 책임과 협력을 정제해가면서 필요한 순간에 객체로부터 역할을 분리해내는 것이 가장 좋은 방법이다.
  • 중요한 것은 책임 이다.
  • 역할을 설계의 중심으로 보는 역할 모델링
    • 상호작용하는 객체들의 협력패턴을 역할들 사이의 협력 패턴으로 추상화 함으로써 유연하고 재사용가능한 시스템을 얻을수 있는 방법

역할과 추상화

  • 추상화를 이용하면 캡슐화가 가능해지고 내부 구현을 숨길수 있다.
    • = 추상화계층을 이용시 중요한 정책을 상위 수준에서 단순화 할수 있다
    • 왜냐면 내부 구현을 숨기고 상위 추상화만을 사용하기때문.
  • 추상화를 쓰면 설계가 유연해진다.
  • 이렇게 추상화를 사용하면 협력이라는 관점에서 세세한 구현사항은 무시하고 추상화에 집중하여 큰 그림을 파학할수 있다.

'book > 오브젝트' 카테고리의 다른 글

5장 - 2. 책임 할당을 위한 GRASP 패턴  (0) 2021.05.09
5장 - 1 책임 할당하기  (0) 2021.05.08
4장 설계 품질과 트레이드오프  (0) 2021.05.05
오브젝트 2장  (0) 2021.04.29
오브젝트 1장  (0) 2021.04.28

2장.

  1. 구현하기 전에 먼저 각 객체들이 어떤 상태인지 어떤 행동을 가지는지를 결정해야한다.
  2. 각 객체들은 독립적인 존재가 아닌 기능 구현을 위한 공동협력체의 일원으로 봐야한다.
  3. 클래스 내부의 인스턴스 변수들은 private으로 선언하여 외부의 접근을 막자.
    • 오로지 적절한 퍼블릭 메소드만을 사용해 내부상태를 변경할수 있게 해야한다.
  4. 내부와 외부를 구분해야하는 이유는 무엇일까 ?
    • 경계의 명확성이 객체의 자율성을 보장한다
      • 무슨소린지 이해를 못하겠음
      • 어느정도 명확한 경계를 통한 자율성의 범위를 정한다는것인지 ?
    • 구현의 자유를 제공한다.
      • 위와 같은 이야기인듯 ? 헷갈림.
  5. 객체는 상태와 행동을 같이 갖는 복합적인 존재
  6. 4번이야기를 좀더하자면 객체의 접근을 통제하면 좀더 자율적이게 됨.
    • 왜냐 다른곳에서 접근할수 없으므로 스스로 상태를 관리, 판단 할수 있게됨
    • 그렇기 떄문에 자율적이게 되는것.
  7. 외부에서 접근 가능한 부분은 퍼블릭 인터페이스
  8. 내부에서만 접근가능한것은 구현 ( implements )
  9. 인터페이스와 구현의 분리 원칙은 훌륭한 객체지향 프로그램을 만들기위해 따라야하는 핵심원칙이다.
  10. 객체가 객체와 통신하는법은 메세지를 전송하는것 뿐이다. 객체가 메세지를 수신하면 수신했다고 한다.
    • 이처럼 수신된 메세지를 처리하기위한 객체 자신만의 방법을 메소드라고 부른다.
  11. 템플릿 패턴을 이용하여 부모클래스에서 로직에대한 구현을 해놓고 자식클래스에서 세부사항을 구현한다.
  12. 전략패턴사용 다양한경우의 조건일 경우 interface를 이용한 전략패턴 사용
  13. 이러한 전략적패턴이라던지 템플릿패턴같은것들 사용하면 디버깅은 어려워지나 유지보수성은 좋아지고 유연성도 늘어난다.
  14. 자식클래스가 부모클래스를 대신하는것을 업캐스팅이라고 한다.
  15. 11~14까지가 다형성
    • 위와같은 부모클래스를 자식클래스들이 상속받아 어떤클래스가 실행되는지에 따라 결과도 달라진다.
  16. 이렇게 런타임시점에 결정되는것을 지연바인딩 또는 동적바인딩이라고 부른다
  17. 컴파일 시점에 결정되는것은 초기바인딩 또는 정적바인딩이라고 한다.
  18. 구현된 코드를 상속받는것은 변경에 취약한 코드다
    • 왜냐면 최상위코드가 변경이되면 하위코드도 다 변경해줘야하기 때문..
  19. 추상화를 사용하면 세부적인 내용을 무시한채 상위 정책을 쉽고 간단하게 표현할 수가 있다.
    • 추상화를 이용해 상위정책을 기술한다는 것은 기본적인 애플리케이션의 협력 흐름을 기술한다는 것을 의미
  20. 추상화를 사용할경우 그 추상클래스나 인터페이스를 상속이나 구현한 어떤 클래스를 추가하면 확장가능해진다.
  21. 이런것들을 컨텍스트 독립성이라고 불린다. 유연한 설계분야에서 진가를 발휘한다.
    • 결론은 유연성이 필요한곳에 추상화를 사용하라
  22. 추상클래스와 인터페이스 중에 어떤것을 고를것인지는 트레이드오프를 통해 정한다.
  23. — 상속 —
    1. 상속은 캡슐화를 위반한다 2. 설계를 유연하지 못하게한다. 위에서 설명함
  24. 캡슐화 위반은 부모클래스의 구현이 자식클래스에게 노출되기 때문.
    • 의문점 디폴트 메소드를 통한 인터페이스의 상속은 왜나온것인가 ?

'book > 오브젝트' 카테고리의 다른 글

5장 - 2. 책임 할당을 위한 GRASP 패턴  (0) 2021.05.09
5장 - 1 책임 할당하기  (0) 2021.05.08
4장 설계 품질과 트레이드오프  (0) 2021.05.05
오브젝트 3장 - 역할, 책임, 협력  (0) 2021.04.30
오브젝트 1장  (0) 2021.04.28

1장.

  1. 객체를 자율화 시켜서 그 객체의 책임이 있는 동작은 객체 스스로 처리하게 하자.
    • 그 객체 로직은 객체 내부에서 동작하자.
    • 그럼 캡슐화가 이루어진다. 외부에서 내부의 동작을 알 필요가 없어짐.
    • 또한 응집도가 높아진다 객체의 책임을 객체가 책임지기 때문에.
    • 결합도가 낮아진다 다른외부에서 이 객체를 가져다 내부작업을 할 필요가 없어졌기 때문에.
  2. 절차지향은 메인클래스에서 모든것을 처리하며 나머지 객체는 단지 데이터보관소에 불과할 뿐이다.
    • 하지만 객체지향은 모든 객체에 자율성이 존재하며 캡슐화가 이루어지며 응집도가 높고 결합도가 낮아진다.
    • 객체지향은 제어의 흐름이 각 객체에 적절하게 분산.
    • 절차지향은 제어의 흐름이 중앙에 한 클래스에 밀집되어있음.
  3. 설계를 어렵게 만드는것은 의존성. 쓸데없는 의존성을 제거해서 객체간의 결합도를 낮추자.
    • 결합도를 낮추기 위해서는 캡슐화를 하고 객체의 응집도도 높이자.
  4. 결합도와 자율성이 충돌되는 시점이 있다.
    • 이때는 트레이드오프로 잘 비교해보고 결정하자.
  5. 객체지향적 설계는 유지보수에 강점을 준다.

'book > 오브젝트' 카테고리의 다른 글

5장 - 2. 책임 할당을 위한 GRASP 패턴  (0) 2021.05.09
5장 - 1 책임 할당하기  (0) 2021.05.08
4장 설계 품질과 트레이드오프  (0) 2021.05.05
오브젝트 3장 - 역할, 책임, 협력  (0) 2021.04.30
오브젝트 2장  (0) 2021.04.29

+ Recent posts