ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Refactoring] 12장 : 상속 다루기
    책/Refactoring 2판 2024. 2. 14. 11:45
    반응형

    메서드 올리기

    // before
    class Parent {
    
    }
    
    class Child1 extends Parent {
        get name() {}
    }
    
    class Child2 extends Parent {
        get name() {}
    }
    
    // after
    class Parent {
        get name() {}   
    }
    
    class Child1 extends Parent {
    }
    
    class Child2 extends Parent {
    }
    • 부모에게는 없는데 자식에게만 중복되서 등장하는 필드, 메서드들이 있는 경우 이를 부모로 올려주는 것이 중복을 막을 수 있다.
    • 만약 동작이 다를 경우, 템플릿 메서드 만들기를 고려할 수 있다.

    필드 올리기

    // before
    class Parent {
    
    }
    
    class Child1 extends Parent {
        private String name;    
    }
    
    class Child2 extends Parent {
        private String name;
    }
    
    // after
    class Parent {
        protected String name; 
    }
    
    class Child1 extends Parent {
    }
    
    class Child2 extends Parent {
    }
    • 위는 자바 코드로, 필드 또한 자식에게만 중복으로 등장하고, 부모에게는 없는 경우 이를 부모로 올려주는 것이 중복을 제거할 수 있는 방법이다.

    생성자 본문 올리기

    // before
    class Parent {
    
    }
    
    class Child1 extends Parent {
        constructor (name, id, cost) {
            super();
            this.id = id;
            this.name = name;
            this.cost = cost;
        }
    }
    
    // after
    class Parent {
        constructor (name) {
            this.name = name;
        } 
    }
    
    class Child1 extends Parent {
        constructor (name, id, cost) {
            super(name);
            this.id = id;
            this.cost = cost;
        }
    }
    • 위 두개의 경우가 생성자에 등장하는 경우이다.
    • 이 경우, 부모 생성자에서 공통 필드를 초기화하는 코드를 추가하고, 서브 클래스에서 해당 부모 생성자를 호출하게 하는 것이 중복을 막을 수 있다.

    메서드 내리기

    // before
    class Parent {
        get name() {}
    }
    
    class Child1 extends Parent {}
    
    class Child2 extends Parent {}
    
    // after
    class Parent {}
    
    class Child1 extends Parent {}
    
    class Child2 extends Parent {
        get name() {}
    }
    • 위와는 반대로, 특정 자식에게만 존재하는 메서드를 위해 부모가 가지고 있는 경우, 이를 해당 자식에게 내려주는 것이 깔끔하다.

    필드 내리기

    // before
    class Parent {
        protected String name;
    }
    
    class Child1 extends Parent {}
    
    class Child2 extends Parent {}
    
    // after
    class Parent {}
    
    class Child1 extends Parent {}
    
    class Child2 extends Parent {
        private String name;
    }
    • 위와 반대로, 특정 자식에게만 존재할 필드를 위해 부모가 가지고 있는 것보다는 이를 특정 자식에게 내려주는 것이 깔끔하다.

    타입 코드를 서브클래스로 바꾸기

    // before
    function createEmployee(name, type) {
        return new Employee(name, type);
    }
    
    // after
    function createEmployee(name, type) {
        switch (type) {
            case "engineer": return new Engineer(name);
            case "salesperson": return new Salesperson(name);
            case "manager": return new Manager(name);
        }
    }
    • 비슷한 대상을 특정 특성에 따라 구분할 때, 이러한 구분자를 타입 코드 필드라고 한다.
    • 만약 타입으로 구분된 각자가 서로 상당히 다른 로직을 구성해야 한다면, 서브클래스로 추출한 뒤 type에 따라 각기 다른 서브 클래스의 객체를 리턴해주는 것이 좋다.
    • 생성자에서는 이런게 불가능하므로, 팩터리 메서드를 사용하는 것이 다시 한번 더 좋음을 볼 수 있다.

    서브클래스 제거하기

    // before
    class Person {
        get genderCode() {return "X";}
    }
    class Male extends Person {
        get genderCode() {return "M";}
    }
    class Female extends Person {
        get genderCode() {return "Y";}
    }
    
    // after
    class Person {
        get genderCode() {
            return this.genderCode;
        }
    }
    • 서브클래스는 원래 데이터 구조와 상당히 다른 동작이나 변종을 다형성으로 만들기 위해 존재한다.
    • 그러나 거의 차이가 없는 경우, 코드량과 복잡도만 높일 뿐이다.
    • 이 경우, 서브클래스들을 슈퍼클래스에 합치는 게 더 나을 것이다.

    슈퍼클래스 추출하기

    // before
    class Department {
        get annualCost() {}
        get name() {}
        get headCount() {}
    }
    
    class Employee {
        get annualCost() {}
        get name() {}
        get id() {}
    }
    
    // after
    class Party {
        get name() {}
        get annualCost() {}
    }
    
    class Department extends Party {
        get annualCost() {}
        get headCount() {}
    }
    
    class Employee extends Party {
        get annualCost() {}
        get id() {}
    }
    • 상속 구조는 설계를 철저히 하고, 부모-자식 관계를 신중히 설계해야 한다는 사실은 맞는 얘기다.
    • 그러나 상속의 경우 프로그래밍을 수행하면서 공통 부분을 발견했을 때 추출하는 경우가 더 많았다고 한다.
    • 슈퍼클래스로 추출하느냐, 단순히 클래스를 하나 추출해서 위임으로 해결하냐 (상속이냐 위임이냐)는 선택의 문제이다.
      • 저자의 추천으로는 먼저 슈퍼클래스로 추출하는 것이 더 간단하고, 만약 맞지 않을 경우에는 슈퍼클래스를 위임으로 바꾸는 것 또한 어렵지 않으므로 그렇게 한다고 한다.

    계층 합치기

    // before
    class Employee {}
    class Salesperson extends Employee {}
    
    // after
    class Employee {}
    • 개발을 하다가 어떤 클래스와 부모가 너무 비슷해져 독립적으로 존재해야할 필요성을 못 느낄 때가 있다.
    • 이 경우 그 둘을 하나로 합치는 게 낫다.

    서브클래스를 위임으로 바꾸기

    // before
    class Order {
        get daysToShip() {
            return this.warehouse.daysToShip;
        }
    }
    
    class PriorityOrder extends Order {
        get daysToShip() {
            return this.priorityPlan.daysToShip;
        }
    }
    
    // after
    class Order {
        get daysToShip() {
            return (this.priorityDelegate) ?
                this.priorityDelegate.daysToShip
                : this.warehouse.daysToShip;
        }
    }
    
    class PriorityOrderDelegate {
        get daysToShip() {
            return this.priorityPlan.daysToShip;
        }
    }
    • 공통 데이터와 동작을 슈퍼 클래스로 빼고, 서브 클래스에는 자신에 맞게 기능을 추가하거나 오버라이드하는 것이 상속의 장점이다.
    • 그러나 상속은 한번만 쓸수 있다. 다중 상속을 지원하지 않는 언어가 많다.
    • 가령 사람 객체의 동작을 나이와 소득 수준에 따라 달리 하고 싶으면 둘다가 안되고, 젊은이 혹은 어르신, 부자 혹은 서민 만 될 수 있다.
    • 또한 상속은 클래스간의 관계를 아주 긴밀하게 결합함
      • 부모를 수정하면 이미 존재하는 자식들의 기능이 바뀔 수 있다.
    • 그래서 상속보다는 컴포지션을 사용하라는 말이 나온다. 컴포지션은 위임과 같은 말이라고 봐도 좋다.
      • 하지만 상속이 당장 바꾸기에는 쉽다. 따라서 상속을 먼저 수행하고, 나중에 필요하면 위임으로 바꾸는 것은 쉽다.
    • 이 리팩터링은 서브클래스를 상태 패턴, 전략 패턴으로 대체하는 것과 비슷
    • 이 리팩터링의 핵심은 생성자를 팩터리 함수로 바꾸는 게 좋다는 것이다.
    • 헷갈리면 안되는게, 이 리팩터링은 자식 객체가 부모한테 들어가는 것이다.

    슈퍼클래스를 위임으로 바꾸기

    // before
    class List {}
    class Stack extends List {}
    
    // after
    class Stack {
        constructor() {
            this.storage = new List();
        }
    }
    
    class List {}
    • 자바의 스택은 리스트를 상속하고 있는데, 이는 큰 문제가 있다.
    • 바로 스택에서는 적용되지 않는 기능들이 스택 인터페이스에 그대로 노출된다.
    • 이러한 경우, 슈퍼클래스, 즉 리스트를 위임, 컴포지션으로 받는 것이 자연스럽다.
    • 리팩터링의 경우는 부모 객체가 자식한테 들어가는 것이다.
    • 저자는 상속을 먼저 하고, 나중에 바꿔야할 때 위임으로 바꾸는 걸 추천한다.

     

    https://product.kyobobook.co.kr/detail/S000001810241

    반응형
Designed by Tistory.