OBJECT 16-2 여러 전략객체를 위한 상속과 다시 합성으로변경
object 책을 강의한 코드스핏츠 유튜브 요약
📜 제목으로 보기
- 구상체들이 다음 타자를 가지는 방법(next 외부체이닝 받기 -> 내부체이닝 메서드 호출)
- 참고 유튜브 : https://www.youtube.com/watch?v=navJTjZlUGk
- 정리본: https://github.com/LenKIM/object-book
- 코드: https://github.com/eternity-oop/object
- 책(목차) : https://wikibook.co.kr/object/
구상체들이 다음 타자를 가지는 방법(next 외부체이닝 받기 -> 내부체이닝 메서드 호출)
달라진 부분 2가지 확인하기
- result라는 재료를 받았지만, 전략객체는 여러개의 call을 돌면서 result에 누적하여
파라미터 result
는 값이 아닌 상태 변화하여 다음 caclulator한테 가고 반환되는, 부수효과가 있는context객체
라는 증거를 확보하여 연결되는 합성객체임을 확인할 수 있다.- 인자가 똑같은 값이 아니라 변하는 인자는 delegate(위임) = 부수효과가 있는 context가 공유되는 것
- context인 result는 현재 전략객체에서
다음 전략객체 next
에게 넘아갈 수도 있다.- 다음 할인이 있다면, 생성자에 다음전략객체가
- 다음 할인이 없다면, 생성자에
null
이 올 수 있다고 생각한다. - 현재 전략객체의 calls돌면서 전략 적용후
next == null? result(리턴)
아니라면: 다음 전략객체.전략메서드( calls, result)
로 바통 넘겨주기
전략객체가 동일형 다음 타자를 받고 꼬리호출로 다음타자의 전략메서드를 호출할 수 있다.
PricePerTime
- 재귀함수처럼, 마지막에 다음 n에서 내일 을 이어받을 함수를 호출한다. 같은 전략메서드 =
주체가 같은형
.반환이 같은 전략메서드()
의꼬리 호출인 마지막 return에서 종착역(null터미네이팅) && 반환값이 같은 전략메서드()로 내부 체이닝하는 것
이다.- 외부에서 체이닝할라면, VO처럼 반환값과 다음 주체가 같은형이어야함
- 내부 체이닝은, return문에 재귀함수처럼 종착역(null터미네이팅) + 같은 반환값의 메서드를 호출해야한다.
NightDiscount
-
마찬가지로, 다음 타자를 가지고 있으면 전략메서드로 넘겨주고, 아니면 결과값을 return
-
Plan은 1개의 Calculator만 알고 있지만, 발동만 시키면, 시작 Calculator(전략객체)가 옆 친구전략객체를 알고있는 것 유무에 따라 더 전진될지 안될지가 결정된다.
- 동적으로 연결된다고 한다.
- 객체지향은 linked List밖에 없다.
- next node가있으면 가고 없으면 스탑이다.
Tex (마찬가지)
- calls 전체에 영향을 안받아서, result context만 갱신시킨다.
AmountDiscount (책: LateDiscountPolicy)
Main
- plan에 맨 처음 적용될 전략객체는
PricePerTime
이고 나머지들이 이어진 다음 전략객체들이 생성자 next자리로 들어온다.- 재귀의 마지막 부분을 터미네이팅시키는
null
을 생성자에 넣어줘야 끝난다.
- 재귀의 마지막 부분을 터미네이팅시키는
데코레이터 패턴은 이상한 모양
- 생성자에서 다음 타자를 받는 이상은 장풍모양이 된다.
중복제거 및 고도화
- 모든 전략객체들이 next를 받아서 저장하고, 관리하는 코드가 똑같다.
- 부모로 묶어야한다.
- 부모로 묶을 때 조심해야할 부분:
공통 상태
(next)를 가지고 있으면 얄쨜없이abstract로 묶어야
한다.
abstract Calculator
- 부모 -> 전략 인터페이스에서 -> 추상클래스로 추출해야한다.
-
구상체(전략객체)들은
다음타자를 생성자
로 받았으나좋은 부모는 생성자로 받질 않는다 -> 초기화없는 필드에 setter받기기능
으로 받아야한다. -
null터미네이팅이 존재하는 필드기 때문에, 다음 타자가 없으면 안넣어주고 그대로 null을 유지하면 된다.
-
또한,
외부 체이닝을 통한 여러개 받기기능
을 위해받기기능 주체자와 동일형의 context인 this를 반환
하여 외부에서 받기기능을 체이닝 할 수 있다.- 내부 체이닝: 재귀함수 -> return에 반환형이 똑같은 메서드로 호출
- **(구상체)매번 다음 타자를 받거나, (추상체)외부체이닝을 통해 여러개를 받은 다음타자들을 -> null터미네이팅과 함께 return꼬리호출부에 다음타자들을 호출시킴. **
- 외부 체이닝: 메서드내 상태 변화 후 메서드를 호출한 주체와 동일한 형(아직 안생긴 객체면 new 생성자() , 이미 존재하는 객체면this)을 반환
- 내부체이닝을 위해, 외부재료를 외부체이닝(return this)하면서 여러개 받기기능
- 내부 체이닝: 재귀함수 -> return에 반환형이 똑같은 메서드로 호출
-
my) 추상체에서 this는
구상체가 사용하며 구상체 자기자신
이다.- next구상체를 받는 메서드 내부라서 헤깔릴 수 있지만, 추상체는 구상체가 쓸 내용만 제공하므로,
추상체 속 this는 호출하는 순간은 구상체 자신
이다.
- next구상체를 받는 메서드 내부라서 헤깔릴 수 있지만, 추상체는 구상체가 쓸 내용만 제공하므로,
- 전략메서드 구현의 다른부분은 다시 훅으로 뿌려주고, 전략 메서드 및 내부 공통 부분을 템플릿 메서드로서 담는다.
좋은 부모인지 확인
- 자식들을 위해 생성자가 없다
- public, final 아니면 abstract proteced 메서드만 있다.
Plan(전략 적용객체)는 코드가 바뀐게 없음.
- 전략객체 받기 -> 템플릿 구상체들 받기
중복제거된 구상체들 확인
PricePerTime
- 기존
- 중복제거 by 추상클래스
- 다음 타자받는 로직이 부모로 넘어간 덕분에, calls + result 계산기능 외에는 본인의 책임만 가진다.
좋은 자식인지 확인
- 상태들 모두 자기것 (부모 것X) -> 부모변화와 무관
- 훅메서드구현으로 부모에게 정보 제공만 함. (부모를 몰름) -> 부모 변화와 무관
- result만 갱신해서 넘겨주는 service 제공
NightDiscount
Tex, AmountDiscount
Main 사용법이 바뀜
- Plan은 확장(상속) 가능성 있는 class로서 받기기능으로 첫번째 템플릿구상체(구 전략객체)를 받기기능으로 받아들이게 된다.
- 첫번째 들어가는 Calculator템플릿구상체는
외부 체이닝 받기기능 in 추상클래스 받기기능에서 return this(메서드 주체를 반환)
을 통해 체이닝으로 2번째 Calculator구상체를 받을 수 있다.- this가 반환되면 또 Calculator이며, Calculator는 원래 setNext(체이닝 받기기능)이 존재한다.
상속을 다시 합성으로
- 상속이 나오면 무조건 합성으로 다시 바꾸기
-
좋은 상속은 기계적으로 합성으로 바꿀 수 있다.
-
Calculator자체가 구상클래스화 -> 훅메서드 부분의 전략객체화
합성(전략)으로 변경되는 Calculator
추상클래스의 this반환을 활용
해서 다음 타자를 외부체이닝으로구상체별 다음타자 받기
를 구현했지만,합성으로 변환하는 순간 추상클래스 -> 구상클래스
가 되므로추상체this를 이용한 구상체별 외부체이닝으로 다음타자 받기
가 불가능해진다.-
추상클래스에서 다음타자next를 소유하는 방법 정리
- 자식이 물려받아서 쓸 next 필드는 초기화없이 null상태로 선언
- 받기기능으로 받되 없으면 null그대로. 있든 없든 추상체this를 return해서 사용 구상체 자기자신 반환
- 사용처에서는 null터미네이팅이 적용된 내부체이닝으로 연쇄호출
- 개별 다음타자 소유 -> 재귀함수 호출
-
next로 소유하는 방법을 버림 -> 위 3가지 다 버림
- 다음타자의 전략메서드 호출을
내부 체이닝 - 재귀함수처럼 호출
했지만,재귀함수는 forloop로
바꿀 수 있다. 개별로 다음타자 소유 -> 재귀함수처럼 호출이 무의미
하다
- 다음타자의 전략메서드 호출을
-
상속-재귀로 다음타자 호출
->합성-forloop로 모든 타자 컬렉션을 호출
-
개별로 다음타자를 가짐 ->
구상클래스가 모든 타자를 컬렉션으로 가짐
으로 변경- cf) HashSet은 순서가 보장된다.
-
여러개 받기기능이라도, 구상클래스이므로
필수 1개를 받도록 생성자 주입후 add
- 하나는 필수이며, 나머지는 list로 받아들이는 경우가 많기 때문에, 이렇게 sample로서 구현
- 이후 여러개 받기기능처럼 setter받기기능으로 add하되,
return 구상체this
는개별 외부체이닝이 아닌 자신 상태를 외부체이닝
- 개별 다음타자 재귀호출이 아니라
- 복잡한 객체간 통신
- 모든 타자 forloop돌면서, result상태 업데이트
-
개별로 다음타자를 가짐 ->
훅메서드 대신 태어난 Calc전략인터페이스
또 달라진 Main 사용법
-
상속 -> 개별로 다음타자 가지기 -> 재귀호출
- 첫번째 구상체객체부터 시작하여, 개별로 다음타자 체이닝해서 받기
-
합성 -> 모든 타자 받기(체이닝 유지) -> forloop호출
- 구상class 1곳에서, 모든 다음타자 체이닝해서 받기
- 상속 모델의 호스팅 -> 구상클래스가 전략객체들을 소유모델로 소유
- 원래 Plan도 Calculator들을 전략객체들을 소유모델이었음.
- 하지만 전략객체들이 중복코드로 상속모델이 됨
- 다시 한번 상속모델을 구상클래스 + 전략객체들 소유모델로 변경
- 하지만 전략객체들이 중복코드로 상속모델이 됨
- 기존: Plan <- 전략Calculator들 소유
- 변경1: Plan <- Calculator부모 <- 자식들
- 변경2: Plan <- Calculator <- 전략Calc들
- 변경1: Plan <- Calculator부모 <- 자식들
-
그럼 Plan도 단일객체고, Calculator도 단일객체니 2개를 합치면 안되나?
- 합성과 상속을 반복해보면, 처음 설계한 역할이 명확하지 않다는 것을 알게 된다.
- DB상 1:1상태 -> 합치면 되는 비정규화 상태
- 의사결정
- Plan이 또다른 계산방식 추가(멤버등급별 요금계산)이 없이, 오로지 Calculator에 의해서만 요금계산이 끝나면, 합쳐도 된다.
- Calculator는 정확하게는 CallsBasedCalculator
- 전략객체로 개별 계산해주되, 큰 로직은 계산의 재료를 calls만 받아서 처리하기 때문이다.
- **개별계산방법이 오로지
전략객체의 전략메서드 특성상 1개의 로직 재료 calls에 대해서만 개별 처리
해주는 것이기 때문에 **
- Calculator는 정확하게는 CallsBasedCalculator
- 하지만, calls이외에 다른 로직에 의해 요금계산도 추가된다면?
-
Plan과 Calculator(calls로 요금계산)을 1:1이라고 합치는 순간 Call에 기반한 요금계산만 Plan에서 이루어지게 된다.
- 전략객체들을 직접적으로 Plan에 주입하면, 카테고리 1개의 개별처리밖에 못한다.
- Plan는 calls이외에 회원등급기반 요금계산도 전략을 통한 개별처리로 처리되어야한다
-
Plan과 Calculator(calls로 요금계산)을 1:1이라고 합치는 순간 Call에 기반한 요금계산만 Plan에서 이루어지게 된다.
- Plan이 또다른 계산방식 추가(멤버등급별 요금계산)이 없이, 오로지 Calculator에 의해서만 요금계산이 끝나면, 합쳐도 된다.
-
개별처리를 위한
적용대상(Plan)
+구상클래스 + 전략객체들
형태를 유지하면다른 카테고리의 개별처리도 적용대상에게 구상클래스로 주입
할 수 있다.-
다른 카테고리의 개별요금처리를 위해서라도, 적용대상 <-> 전략구상클래스를 합치지말자.
- 요금계산에는 멤버쉽, 국가유공자, 나이별, VIP(충성고객) 여러 카테고리가 남아있다.
-
다른 카테고리의 개별요금처리를 위해서라도, 적용대상 <-> 전략구상클래스를 합치지말자.
- 의사결정하는 가운데, 실제이름도 알게 된다. Calculator는 포괄적인 이름이다. 전략패턴이 담당하는 카테고리가 있다.
~Based
전략메서드가 처리담당하는 메서드 인자로 판단하여~기반의 개별처리
밖에 안된다.- Plan과 Calculator가 단일class라고 해서 동급의 레벨이 아니였다.
-
Plan
- 한참 아래의 Call기반의
Calculator
: **Plan이 골라야하는 메뉴중 1개 **-
calc
들
-
- 한참 아래의 Call기반의
-
- Plan과 Calculator가 단일class라고 해서 동급의 레벨이 아니였다.
- 다시 메인을 보면
- plan에 들어가는 Calculator는 일반명사처럼 생겼지만,
콜 기반의 요금 계산
만 해주는콜 카테고리 계산기
로서 1개의 계산기일 뿐이다. - Calculator는 plan입장에서는 여러가지 요금계산 계획중에 1개의 요금계산을 위임한 녀석이다.
-
CalculatorBasedOnCalls
- call에 따른 3가지 전략(개별처리)들을 처리해주고 있다.
-
- new Calculator(콜기반요금계산) 외에
- Plan은 new CalculateBasedMemebr()와도 결합할 수 있을 것이다.
- plan에 들어가는 Calculator는 일반명사처럼 생겼지만,
- Plan - Calculator - 전략Calculator의 3단이 맞냐?
-
이름에 현혹되지마라. 3단이 맞다.
- Plan +
Call기반 구상클래스 + 주입되는 전략객체들
- Plan +
- Calculator라는 추상화된 좋은 이름을 가질 수있냐 의심해봐야한다. 계산하되 call기반으로만 계산하고 있으며
다른 카테고리의 CalculateFee도 할 수 있다.
-
일반명사로 만들었다면, 의심해라 나는 자격이 없다.
- Calculator정도의 이름은 팀장아니면 실장만 달 수 있다.
-
이름에 현혹되지마라. 3단이 맞다.
합성(구상클래스 + 전략객체들)했을 때의 유혹
- Plan이 Calculator와 직접적은 결합은 하지 않지만,
내부결합의 유혹
을 많이 받는다.- 왜냐면,
다른 카테고리의 Calculator가 나오기 전
까지plan만들 때마다 new Calculator()
가 반복된다. -
어차피 plan만들면, setCalculator( new Calcuator())를 할 것이니 내부에 감추고 싶다.
+나머지 추가 전략객체들을 받는 것도 뻔하게 나머지 전략객체들을 setNext하겠지
의 유혹- 성급한 최적화다.
- 왜냐면,
성급한 최적화(전략객체를 받아주는 구상클래스를 내부로 통합)
-
일반명사를 가지며, 바뀌지 않을 것 같고 반복되는
->전략주입용 구상클래스
Calculator를 변하지 않는 것으로 간주하며, 반복된다고 생각하여 Plan내부로 넣는 성급한 최적화를 하고 싶은 유혹에 빠져든다.- 하지만, Calculator는 사실 Calculator
BasedCalls
이며,전략주입 구상클래스는 다른 전략으로 바뀔 수 있는 클래스
임을 망각한 것이다.
- 하지만, Calculator는 사실 Calculator
- setCalculator가 아니라, 전략객체들을 가변배열로 받는 setCalculators()로 주입한다.
- new Calculator()부분을 client에서 제거함.
-
전략객체들을 for문으로 돌되,
첫번째 요소를 지나면 flag를 false로 놓고 작동안하게 하는 isFirst플래그
를 만들어놓고 for문을 돌면서- 첫번째 전략객체만 -> 전략주입 구상클래스의 필수 1개 생성자로 주입되고
- 2번째부터는 구상클래스.setNext( 다음 전략 ) 외부체이닝으로 받아준다.
-
그러나 전략패턴이
구상클래스 + 주입전략객체들 + 전략인페
의 구성이 이루어졌다면- 보통 같으면 구상클래스 없이 전략인페 + 주입전략 객체들이지만
-
구상클래스를 둠으로서
여러 전략객체들을 여러개 적용
할 수 있는 구조를 마련했다면- 그 구상클래스도 전략의 일부분일 뿐이며, 일반명사로 쓰고 있다고 해서 바뀌지 않는 부분이 아니다. 전략적용 객체에 주입될 때, 언제든지
다른 카테고리의 전략주입 구상클래스
로 대체될 수 있음을 생각하자.
- 그 구상클래스도 전략의 일부분일 뿐이며, 일반명사로 쓰고 있다고 해서 바뀌지 않는 부분이 아니다. 전략적용 객체에 주입될 때, 언제든지