상속 다루기는 다음과 같은 내용을 담고 있다.
많네...
동기 : 동일한 기능을 하는 함수를 여러개 둘 경우 관리하기 힘들다. 하나가 변경되면 나머지 함수들도 변경을 고려해야하기 때문이다.
그래서 메소드 올리기
의 핵심은 동일한 기능의 코드라면
부모클래스에서 관리하자 이다.
위의 절차를 요약하자면 메소드 올리기는 크게 3가지로 분류된다
1. 그냥 같은 기능을 하는 함수 -> 상위 클래스로 바로 올린다
2. 리턴값은 다르지만 거의 같은 기능을 하는 함수 -> 함수 매개변수화
3. 큰 로직은 같은데, 함수마다 개성이 넘칠때 -> 템플릿 메소드
//변경전
void goBusan();
void goSeoul();
//변경후
void goTo(String region);
abstract class Book{
abstract public void login();
void book(){
login();
anything();
}
}
class KakaoBook extends Book {
@Override
public void login(){
kakaoLoginCheck();
}
}
class NaverBook extends Book {
@Override
public void login(){
naverLoginCheck();
}
}
abstract class BookFactory {
public static KakaoBook createKakaoBookInstance(){
return new KakaoBook();
}
public static NaverBook createNaverBookInstance(){
return new NaverBook();
}
}
template method pattern + abstract Factory pattern 을 이용한 결과만 보여줬는데, book 에 해당하는 함수 로직들은 login을 제외하고는 동일한 로직을 가졌다. 즉 공통로직을 전부 상위 클래스로 올리고, 다른 로직만 하위클래스로 책임을 넘겼다고 보면 될 것 같다.
서브클래스 책임오류
서브클래스에서 부모클래스의 메소드가 필요하지 않은 겨웅가 있다. 이럴때는 구현되지 않음에 대한 표시를 해줘야하는데, 이를 보통 오류로 한다. 이런 오류를Subclass Reponsibility Error
라고한다.
근데 이렇게 구현하면 리스코프 치환 법칙을 만족했다고 해야하나? 만족하지 않는다고 해야하나? -> 설계적 관점에서 보면 어긋난다고 생각되는게 개인 의견.
동기 : 나중에 독립적으로 만들어진 서브클래스거나, 계층적으로 클래스가 구성된 경우에는 공통의 필드를 하위클래스에서 가진 경우가 있다. 이런 공통적인 필드를 상위 클래스에서 편하게 관리하자
class Employee {
}
class Daeri extends Employee {
private List<Grade> grades;
}
class Bujang extends Employee {
private List<Grade> grades;
}
class Employee {
private List<Grade> grades;
}
class Daeri extends Employee {
}
class Bujang extends Employee {
}
이렇게 둠으로써 데이터의 중복선언
과 해당필드에 관련된 메소드
들을 슈퍼클래스에서 공통적으로 관리하게 둘 수 있다.
무작정 올리기 보다는 클래스를 잘 나타내는 도메인인지 확인하고, 그것을 잘나타내는 도메인일 경우에만 모는 것이 적당할 것 같다.
동기 : (책에는 나와있지 않지만 메소드 올리기와 상이하다 생각든다. 하지만 추가로 생성자는 다루기 까다롭기 때문에 따로 장으로 분류 해놓은 것 같다. ) 생성자는 할수 있는 일과 호출순서에 제약이 있기 때문에 조심스럽게 접근해야함.
생성자 관련해서 힘들었던 경험이 있으셨음? 물어보자
있었던것 같은데 잘 기억나지 않아서 공감되는 사례랑 들으면 더 와닿을 것 같음
class A {
public A(String name, int age){
this.name = name;
this.age = age;
}
}
class B extends A{
//변경전
public B(String name, int age, String address){
this.name = name;
this.age = age;
this.address = address;
}
//변경후
public B(String name, int age, String address){
super(name, age);
this.address = address;
}
}
동기 : 상위 클래스의 메소드가 모든 자식 클래스에 적용되는게 아닌 특정 클래스에 적용이 된다고 한다면, 상위클래스의 메소드를 아래로 내려 관리한다. -> 응집도
up, 결합도
down
예시 생략
책임을 구분할 수 있는 판단력을 기르고 생각을 많이하자
메소드 내리기와 동일하다. 특정 서브클래스에 해당하는 필드라면 해당 필드로 내린다.
동기 : 보통 타입코드를 처리하는 곳은 로직을 한대 모으는 경우가 많기때문에, 일반적으로 타입코드가 포함된 로직을 처리하는 함수는 굉장히 비대해진다. (타입코드를 처리하는 부분이 파편화되면,, 뜯어 고치자..) 굉장히 비대해진 함수는 이해하기도, 고치기도 굉장히 난해한 경우가 많다. 이를 분리해보자
메소드 올리기랑 예시가 얼추 같다. 그대로 들고와도 될듯?
대신 저자가 원하는 방법대로 변경하면 다음과 같을 것 같다.
enum LoginType{
NAVER, KAKAO
}
class Book{
private LoginType loginType;
public void book(){
login();
anything();
}
private void login(){
LoginService loginService = getLoginServiceInstance();
loginservice.login();
}
private LoginService getLoginServiceInstance(){
switch(loginType){
case LoginType.NAVER:
return new NaverLoginService();
case LoginType.KAKAO:
return new KakaoLoginService();
default:
throw new IllegalArgumentException("invalid login type error");
}
}
}
class KakaoLoginService implements LoginService {
@Override
public void login(){
kakaoLoginCheck();
}
}
class NaverLoginService implements LoginService {
@Override
public void login(){
naverLoginCheck();
}
}
// 나머지 Book을 상속한 KakaoBook. NaverBook 만드는건 생략
login에 관한 부분이 확실히 빼서 관리할 수 있다. Book 은 로그인에 대한 부분은 신경쓰지 않아도 된다.
-> 응집도 Up
동기 : 프로그래밍을 하다보면, 특정 제품이나 도메인이 페이드 아웃되면서 쓰이지 않을때가 있다. (위의 예를들면 더는 예약에 있어 login check가 필요하지 않는다던가) 이런경우 서브클래스를 다시 하나의 클래스로 관리하면 유지보수하기 편해진다.
누가 그러더라 조립은 분해의 역순이라고
근데 분해해놓고 다시 조립하면 못하는게 현실
암튼 절차만 적고 생략하겠다.
동기 : 코드를 작성하다보면 두클래스가 어떤 연관관계를 가지고 있진 않지만, 하는 행동이 비슷해보이는 친구가 있을수도 있다. 두친구들의 공통 특성을 모으고 슈퍼클래스로 추출하도록하여 관리 편하게 한다.
써야할 코드가 많을 것 같아서 예시를 들어보자. 웹브라우저의 Cookie, Storage를 관리하는 각각의 CookieUtil
, StroageUtil
클래스를 구현했다고하자.
생각해보면 둘다 공통적인 기능이 많다.
<key, value> 저장소에 expire time 설정까지, 암튼 이런것을 공통적으로 풀어나갈수 있을 것 같다
class KeyValueUtil {
void setValue(String key){
}
Object getValue(String key){
}
}
class CookieUtil extends KeyValueUtil {
}
class StorageUtil extends keyValueUtil {
}
근데 생각해보니, 해당 기능가지고는 단순 class 보다는 interface가 더적합하다고 본다.
keyValueUtil에서 Caching 기능을 지원한다던지, 어플리케이션 단에서 공통적인 특정 기능이 있어야 하는게 오히려 다 잘 어울릴 듯.
동기 : A extends B extends C extends D, 알기 어렵다, 쓰임새도 명확하지 않다면, 합쳐라!
끝
동기 : 큰 로직은 비슷한데, 동작이 달라지는 객체들은 상속으로 표현하는게 자연스럽다. 하지만 상속은 너무 비용이크다. 상속을 한다는 것 자체가 부모클래스와의 거의 완전한 결합
을 의미하며 하위 클래스의 동작을 알기 위해서는 부모클래스의 동작까지도 빠짐없이 잘알아야한다
. 또한 서브클래스를 만든다는 것 자체가 큰 비용
을 유발한다. 특정 동작만 변경하고 싶은데, 부모클래스의 모든 클래스를 구현해야할 수 도있다. 이럴때 생성 비용을 최소화할 수 있는 방법이 Delegate
이다.
넘 양이 많아...
class Booking {
BookingDelegate bookingDelegate;
public getPrice(){
return bookingDelegate != null ? bookingDelegate.getPrice() :this.price;
}
}
class BookingDeletegate {
Booking booking;
public BookingDelegate(Booking booking){ //역참조가 일어날 수도 있다.
this.booking = booking;
}
public getPrice(){
return booking.getPrice() + getPremiumprice();
}
private getPremiumPrice(){
return 1000;
}
}
Delegate 클래스를 둠으로써 변경은 최소화하고, getPrice부분만 변경하도록 할 수 있다.
동기 : 아까 부킹이라 쳐보자, 예약에 관한 delegate, 조회에 관한 Delegate, 가격에 대한 Delegate, 각기능 마다 Delegate 할꺼면 위임해라 ㅅㄱ ㅂㅇ
귀찮다. 생략
처음에는 분리하고 나중에는 다시 합치라하고 이 책의 공통적인 내용이다.
컴포지션을써라, 아니다 상속을써라, 떼라, 붙여라..등등
일관적이지 못한 저자의 태도를 비판해볼만하다.
감히 내가 뭐라고 비판하겠냐, 그냥 하라면 해야지.
그럼에도 불구하고 코드를 붙였다 뗏다하는 내용이 반복적으로 나온 이유는
독자에게 분리하고 분리안하고의 책임을 넘김(현실 Abstract Method Pattern good..)과 동시에 많은 생각과 고민을 해서 좋은 코드를 짜라는게 목적이 아닌가 싶다.
아무튼 읽으면서 분리 하고 분리안하고의 내 주관적인 기준은 내가 아니더라도 읽기 쉬운 코드인가
가 분리의 기준이 될 것 같다.
여러분들도 어떤 기준으로 좋은 코드를 짜고 있는지 잘 생각하고 고민하셔서 코드 똥내 나는 깃허브 민폐남이 되지 않도록 주의하자.