Skip to content

다형성

Published: at 오전 07:33

Table of contents

Open Table of contents

다형성

다형성의 분류

다형성의 분류.png

상속의 양면성

상속의 목적은 코드 재사용이 아니다. 상속은 프로그램을 구성하는 개념들을 기반으로 다형성을 가능하게 하는 타입 계층을 구축하기 위한 것이다.

상속을 사용한 강의 평가

public class GradeLecture extends Lecture {
    private List<Grade> grades;

    public GradeLecture(String name, int pass, List<Grade> grades) {
        super(name, pass);
        this.grades = grades;
    }

    @Override
    public double evaluate() {
        return grades.stream()
                .mapToInt(Grade::getPoint)
                .average()
                .orElse(0);
    }
}

public class Lecture {
    private String name;
    private int pass;

    public Lecture(String name, int pass) {
        this.name = name;
        this.pass = pass;
    }

    public String evaluate() {
        return "Pass: " + pass + ", Fail: " + (grades.size() - pass);
    }

    public String stats() {
        return "Pass: " + pass + ", Fail: " + (grades.size() - pass);
    }

    public String getName() {
        return name;
    }

    public int getPass() {
        return pass;
    }

    public List<Grade> getGrades() {
        return grades;
    }

    public void setGrades(List<Grade> grades) {
        this.grades = grades;
    }

    public void setPass(int pass) {
        this.pass = pass;
    }
}

데이터 관점의 상속

Java와 JavaScript로 생각해보는 데이터 관점의 상속

Java

  • 상속을 통해 부모 클래스의 인스턴스를 자식 클래스의 인스턴스 안에 포함시킨다
  • 객체 구조가 컴파일 타임에 결정되기 때문에 메모리 레이아웃을 고정 시킬 수 있다.
  • 부모 클래스의 필드와 메서드가 자식 클래스의 인스턴스 내에 포함된다.

JavaScript

  • 프로토타입을 통해 부모 클래스의 인스턴스를 자식 클래스의 인스턴스 안에 포함시킨다.
  • 객체 구조가 런타임에 결정되기 런타임에 프로토타입 체이닝을 통해서 부모 클래스의 인스턴스를 찾아간다.
  • 인스턴스는 부모 클래스와 자식 클래스의 프로퍼티와 메서드에 접근할 수 있지만, 이는 proto 체인을 통해 이루어진다.
  • 부모 객체의 프로퍼티와 메서드를 복사하지 않고 참조하기 때문에 메모리 효율성이 높다.
  • 런타임에 객체의 구조를 변경할 수 있다.

행동 관점의 상속

모든 인스턴스가 동일한 몇가지 동일한 속성을 공유하는 경우, 프로토타입의 강점이 드러납니다. 이는 특히 메서드를 공유할 경우 더욱 두드러집니다. 각 인스턴스에는 중복되고 불필요한 작업을 수행하는 고유한 함수 속성이 있기 때문에, 기대에 미치지 못하는 코드가 됩니다. 모든 상자의 getValue 메서드가 동일한 함수를 참조하므로, 메모리의 사용량이 줄어듭니다. 그러나 모든 객체 생성에 대해 proto를 수동으로 바인딩하는 것은 여전히 매우 불편합니다. 이것은 생성된 모든 객체에 대해 [[Prototype]]을 자동으로 설정하는 constructor 함수를 사용하는 경우입니다.

상속과 프로토타입

클래스와 인스턴스의 개념적인 관계.png

업캐스팅과 동적 바인딩

개방-폐쇄 원칙과 의존성 역전 원칙 업캐스팅과 동적 메서드 탐색은 코드를 변경하지 않고도 기능을 추가할 수 있게 해준다.

업캐스팅

컴파일러 관점에서 자식 클래스는 아무런 제약 없이 부모 클래스를 대체할 수 있다.

동적 바인딩

함수를 호출하는 전통적인 언어들은 호출될 함수를 컴파일 타임에 결정한다. 코드 상에서 bar라는 함수를 호출하면 실제로 실행되는 코드는 bar라는 함수다

동적 메서드 탐색과 다형성

객체지향 시스템이 실행할 메서드를 선택하는 규칙

객체가 메시지를 수신하면 self 참조라는 임시 변수를 자동으로 생성한 후 메시지를 수신한 객체를 가리키도록 설정한다.

메서드 오버로딩

대부분의 사람들은 하나의 클래스 안에서 같은 이름을 가진 메서드들을 정의하는 것음 메서드 오버로딩으로 생각하고 상속 계층 사이에서 같은 이름을 가진 메서드를 정의하는 것은 메소드 오버로딩으로 생각하지 않는 경향이 있다.

C++에서는 같은 클래스 안에서의 메서드 오버로딩을 허용하지만 자바와 달리 상속 계층 사이에서의 메서드 오버로딩은 금지한다.

동적인 문맥

public class Lecture {
    public String stats() {
        return "Pass: " + pass + ", Fail: " + (grades.size() - pass);
    }

    public String getEvaluationMethod() {
        return "Pass or Fail";
    }
}

public class GradeLecture extends Lecture {
    @Override
    public String getEvaluationMethod() {
        return "Grade";
    }
}

public class Main {
    public static void main(String[] args) {
        Lecture lecture = new GradeLecture();
        System.out.println(lecture.stats());
    }
}

self전송은 self 참조부터 탐색을 다시 시작하게 만든다

self 전송은 깊은 상속 계층과 계층 중간중간에 함정처럼 숨겨져 있는 메서드 오버라이딩과 만나면 극단적으로 이해하기 어려운 코드가 만들어진다.

이해할 수 없는 메시지

정적 타입 언어와 이해할 수 없는 메시지

코드를 컴파일할 때 상속 계층 안의 클래스들이 메시지를 이해할 수 있는지 여부를 판단한다.

동적 타입 언어와 이해할 수 없는 메시지

협력을 위해 메시지를 전송하는 객체는 메시지를 수신한 내부구현에 대해선 알지 못하기 때문이다. 하지만 동적인 특성과 유연성은 코드를 이해하고 수정하기 어렵게 만들고 디버깅 과정을 복접하게 만들기도 한다.

동적 타입 언어에서 이해할 수 없는 메시지를 어떻게 처리할 수 있을까?

class Apple {
  getApple() {
    return "apple";
  }
}

const apple = new Apple();

apple.getApple(); //=> apple
apple.getBanana(); //=> TypeError: apple.getBanana is not a function

const newApple = new Proxy(apple, {
  get: (target, prop, receiver) =>
    prop in target
      ? target[prop]
      : () => console.log(`Method ${prop} does not exist on target`),
});

newApple.getApple(); //=> apple
newApple.getBanana(); //=> Method getBanana does not exist on target

prop in target vs target.hasOwnProperty(prop) > prop in target은 target 객체의 프로퍼티를 상속받은 프로퍼티까지 검사한다. (프로토타입 체인까지 체크) target.hasOwnProperty(prop)은 target 객체의 프로퍼티만 검사한다.

self 대 super

상속 대 위임

class Lecture
  def initialize(name, scores)
    @name = name
    @scores = scores
  end

  def stats(this)
    "Name: #{@name}, Evaluation Method: #{this.getEvaluationMethod ()}"
  end

  def get_evaluation_method()
    "Pass or Fail"
  end
end

class GradeLecture
  def initialize(name, canceld, scores)
    @parent = Lecture.new(name, scores)
    @canceld = canceld
  end

  def stats(this)
    @parent.stats(this)
  end

  def get_evaluation_method()
    "Grade"
  end
end

프로토타입 기반의 객체지향 언어

객체지향은 객체를 지향하는 것이다. 클래스는 객체를 편리하게 정의하고 생성하기 위해 제공하는 프로그래밍 구성 요소일 뿐이며 중요한 것은 메시지와 협력이다. 클래스 없이도 객체 사이의 협력 관계를 구축하는 것이 가능하며 상속 없이도 다형성을 구현하는 것이 가능하다. 중요한 것은 클래스 기반의 상속과 객체 기반의 위임 사이에 기본 매커니증을 공유한다는 점이다.