다형성: 런타임에 메시지를 처리하기에 적합한 메서드를 동적으로 탐색하는 과정을 통해 구현된다.
상속: 메서드를 찾기 위한 일종의 탐색 경로를 클래스 계층의 형태로 구현하기 위한 방법이다.
이번장에서는 다음을 알아보자!
- 타입 계층의 개념
- 다형적인 타입 계층을 구현하는 방법
- 올바른 타입 계층을 구성하기 위해 고려해야 하는 원칙
public class Money {
public Money plus(Money amount) { ... }
public Money plus(BigDecimal amount) { ... }
public Money plus(long amount) { ... }
}
정수 + 문자열
+
)가 연결연산자로 동작한다.List
인터페이스 (List<T>
); 인스턴스 생성 시점에 T
를 구체적인 타입으로 지정한다.public class Movie {
private DiscountPolicy discountPolicy;
public Money calculateMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
상속은 클래스들을 계층으로 쌓아 올린 후 상황에 따라 적절한 메서드를 선택할 수 있는 메커니즘을 제공한다.
객체가 메시지를 수신하면 객체지향 시스템은 메서드를 처리할 적절한 메서드를 상속 계층 안에서 탐색한다.
실행할 메서드의 선택은 아래의 영향요소에 의해 달라진다.
이번 장에서는
- 포함 다형성의 관점에서 런타임에 상속 계층 안에서 적절한 메서드를 선택하는 방법을 이해해보자.
- 이번 장에서 다루는 내용은 (상속 이외에도) 포함 다형성을 구현할 수 있는 다양한 방법에 공통적으로 적용할 수 있는 개념이다.
Pass:3 Fail:2, A:1 B:1 C:1 D:0 F:2
Pass:3 Fail:2
: 강의를 이수한 학생의 수, 낙제한 학생의 수A:1 B:1 C:1 D:0 F:2
: 등급별 학생 분포 현황Lecture
라는 클래스가 먼저 구현되어 있는 상황.public class Lecture {
private int pass;
private String title;
private List<Integer> scores = new ArrayList<>();
public Lecture(String title, int pass, List<Integer> scores) {
this.title = title;
this.pass = pass;
this.scores = scores;
}
public double average() {
return scores.stream()
.mapToInt(Integer::intValue)
.average().orElse(0);
}
public List<Integer> getScores() {
return Collections.unmodifieableList(scores);
}
public String evaluate() {
return String.format("Pass:%d Fail:%d", passCount(), failCount());
}
private long passCount() {
return scores.stream().filter(scroe -> score >= pass).count();
}
private long failCount() {
return scores.size() - passCount();
}
}
객체지향 프로그래밍
과목의 수강생 5명에 대한 성적 통계Lecture lecture = new Lecture("객체지향 프로그래밍",
70,
Arrays.asList(81, 95, 75, 50, 45));
String evaluration = lecture.evaluate(); // 결과 => "Pass:3 Fail:2"
GradeLecture
Lecture
의 출력결과에 등급별 통계를 추가하는 것Lecture
를 상속받아 재사용evaluate
오버라이딩: 학생들의 이수여부 + 등급별 통계super
- 자식 클래스 내부에서 부모 클래스의 인스턴스 변수나 메서드에 접근하는데 사용된다.
- 정확하게는, 가시성의
public
이나protected
인 인스턴스 변수와 메서드에만 접근 가능하다.- 가시성이
private
인 경우에는 접근할 수 없다.
public class GradeLecture extends Lecture {
private List<Grade> grades;
public GradeLecture(String name, int pass, List<Grade> grades, List<Integer> scores) {
super(name, pass, scores);
this.grades = grades;
}
// 부모 메서드와 시그니처 동일: 오버라이딩(overriding)
@Override
public String evaluate() {
return super.evaluate() + ", " + gradesStatistics(); //** super 호출
}
@Override
private String gradesStatistics() {
return grades.stream()
.map(grade -> format(grade))
.collect(joining(" "));
}
private String format(Grade grade) {
return String.format("%s:%d"m grad.egetName(), gradeCount(grade));
}
private long gradeCount(Grade grade) {
return getScores().stream()
.filter(grade::include)
.count();
}
// 부모 메서드와 메서드명 동일, 파라미터 다름: 오버로딩(overloading)
public double average(String gradeName) {
return grades.stream()
.filter(each -> each.isName(gradeName))
.findFirst()
.map(this::gradeAvarage)
.orElse(0d);
}
private double gradeAverage(Grade grade) {
return getScores().stream()
.filter(grade::include)
.mapToInt(Integer::intValue)
.average()
.orElse(0);
}
}
public class Grade {
private String name; // 이름
private int upper, lower; // 등급범위: 최소성적, 최대성적
private Grade(String name, int upper, int lower) {
this.name = name;
this.upper = upper;
this.lower = lower;
}
public String getName() {
return name;
}
public boolean isName(String name) {
return this.name.equals(name);
}
public boolean include(int score) {
return score >= lower && score <= upper;
}
}
Lecture lecture = new GradeLecture("객체지향 프로그래밍",
70,
Arrays.asList(new Grade("A", 100, 95),
new Grade("B", 94, 80),
new Grade("C", 79, 70),
new Grade("D", 69, 50),
new Grade("F", 49, 0),
Arrays.asList(81, 95, 75, 50, 45));
// 결과 => "Pass:3 Fail:2, A:1 B:1 C:1 D:1 F:1"
lecture.evaluate();
Lecture
의 average
와 GradeLecture
의 average
Lecture lecture = new Lecture("객체지향 프로그래밍",
70,
Arrays.asList(81, 95, 75, 50, 45));
title
, pass
, scores
)를 저장할 메모리 공간 할당lecture
변수에 대입 Lecture lecture = new GradeLecture("객체지향 프로그래밍",
70,
Arrays.asList(new Grade("A", 100, 95),
new Grade("B", 94, 80),
new Grade("C", 79, 70),
new Grade("D", 69, 50),
new Grade("F", 49, 0),
Arrays.asList(81, 95, 75, 50, 45));
lecture
는 GradeLecture
의 인스턴스를 가리키기 때문에, 특별한 방법 없이는 내부의 Lecture
에 접근할 수 없다.그림에서 인스턴스는 2개가 생성되었으나, 클래스는 단 하나만 메모리에 로드되었다.
Lecture
의 위치를 가리키는 class
라는 이름의 포인터를 가진다.Lecture
는 자신의 부모 클래스인 Object
의 위치를 가리키는 parent
라는 이름의 포인터를 가진다.그림은 어떻게 클래스의 인스턴스를 통해 부모 클래스에 정의된 메서드를 실행할 수 있는지 보여준다.
class
포인터로 연결된 자신의 클래스에 적절한 메서드가 존재하는지 찾는다.parent
포인터를 따라 부모 클래스를 훑어, 적절한 메서드 존재여부를 검색한다.class
포인터와 parent
포인터를 조합하면, 최상위 부모 클래스에 이르기까지 모든 부모 클래스에 접근 가능.⚠️ 주의사항
- 그림들은 상속을 이해하기 쉽도록 표현한 개념적인 그림이다.
- 구체적인 구현 방법과 메모리 구조는 언어와 플렛폼에 따라 다르다.
Lecture
의 evaluage
와 average
를 호출한다.public class Professor {
private String name;
private Lecture lecture;
public Professor(String name, Lecture lecture) { // ** 생성자에서는 Lecture를 받는다.
this.name = name;
this.lecture = lecture;
}
public String compileStatistics() {
return String.format("[%s] %s - Avg: %.1f", name,
lecture.evaluate, lecture.average());
}
}
다익스트라 교수가 강의하는 알고리즘 과목의 성적 통계 계산 코드
Lecture
를 이용하는 경우[다익스트라] Pass:3 Fail:2 - Avg: 69.2
Professor professor = new Professor("다익스트라",
new Lecture("알고리즘",
70,
Arrays.asList(81, 95, 75, 50, 45)));
// 결과 => "[다익스트라] Pass:3 Fail:2 - Avg: 69.2"
String statistics = professor.compileStatistics();
GradeLecture
를 이용하는 경우Professor professor = new Professor("다익스트라",
new GradeLecture("알고리즘",
70,
Arrays.asList(new Grade("A", 100, 95),
new Grade("B", 94, 80),
new Grade("C", 79, 70),
new Grade("D", 69, 50),
new Grade("F", 49, 0),
Arrays.asList(81, 95, 75, 50, 45)));
// 결과 => "[다익스트라] Pass:3 Fail:2, A:1 B:1 C:1 D:1 F:1 - Avg: 69.2"
String statistics = professor.compileStatistics();
생성자는 Lecture로 선언되어 있지만, GradeLecture를 전달해도 문제없이 실행된다.
위 예제는 lecture에 대해 동일한 evaluate 메시지를 전송했을 때, 동일한 코드안에서 서로 다른 클래스 안에 구현된 메서드를 실행할 수 있다는 사실을 보여준다.
Lecture
) 타입으로 선언된 변수에 자식 클래스(GradeLecture
)의 인스턴스를 할당하는 것.업캐스팅, 동적 바인딩 덕분에 코드를 변경하지 않고서도 메시지에 실행되는 실제 메서드를 변경할 수 있다.
개방-폐쇄 & 의존성 역전
- 최종적 목표: 코드를 변경하지 않는 기능 추가
- 개방-폐쇄 원칙: 확장 가능한 코드를 만들기 위해 의존관계를 구조화하는 방법을 설명
- 업케스팅, 동적 메서드 탐색: (상속을 이용한 개방-폐쇄 원칙을 따르는 코드 작성 시) 하부에서 동작하는 기술적 내부 메커니즘을 설명
- 개방-폐쇄 원칙이 목적이라면, 업캐스팅, 동적 메서드 탐색은 목적에 이르는 방법이다.
- 현재
Professor
는 구체 클래스인Lecture
에 의존하므로, 의존성 역전 원칙을 따르는 상황은 아니다.
- 개방-폐쇄 원칙의 중심에는 추상화가 있기 때문이다.
모든 객체지향 언어는 명시적으로 타입변환을 하지 않아도 부모 클래스 타입의 참조변수에 자식 클래스의 인스턴스를 대입 가능하도록 허용한다.
Lecture lecture = new GradeLecture(...);
public class Professor {
public Professor(String name, Lecture lecture) { ... }
}
// 파라미터 중 Lecture 자리에 GradeLecture 전송
Professor professor = new Professor("다익스트라", new GradeLecture(...));
Lecture lecture = new GradeLecture(...);
GradeLecture GradeLecture = (GradeLecture)lecture;
Lecture
의 모든 자식 클래스는 evaluate
메시지를 이해할 수 있으므로(이 전제조건이 존재할 때), Professor
는 Lecture
를 상속받는 어떤 자식 클래스와도 협력할 수 있는 무한한 확장 가능성(이 이점이 존재한다)을 가진다.bar
함수를 호출하는 구문이 나타나면, 실제 실행되는 코드는 바로 그 bar
함수다. 이외의 어떤 코드도 아니다.foo.bar()
라는 코드에서 실제 어떤 클래스의 어떤 메서드가 실행될지 판단하기 어렵다.foo
가 가르키는 객체가 가르키는 인스턴스인지, 2. bar
메서드가 해당 클래스의 상속 계층의 어디에 위치하는지를 알아야 한다.객체지향 언어는 어떤 규칙에 따라 메서드 전송과 메서드 호출을 바인딩하는 걸까?
self
참조라는 임시 변수를 자동으로 생성한다.self
가 가리키는 객체의 클래스에서 시작self
참조는 자동 소멸class
포인터, parent
포인터, self
참조를 조합하여 메서드를 탐색한다.메서드 탐색은 자식클래스에서 부모 클래스의 방향 (자식 -> 부모)으로 진행된다.
항상 자식 클래스의 메서드가 먼저 탐색되므로, 자식 클래스에 선언된 메서드가 부모보다 더 높은 우선순위를 가지게 된다.
self 와 this
- 정적 타입 언어(C++, 자바, C#)
self
참조를this
라고 부른다.- 동적 타입 언어(스몰토크, 루비)
self
참조르 나타내는 키워드로self
를 사용한다.
GradeLecture
인스턴스의 메모리 상태self
참조에서 상속 계층을 따라 이뤄지는 동적 메서드 탐색을 표현self
참조가 가리키는 메모리class
포인터class
포인터 따라 이동GradeLecture
클래스의 정보를 읽을 수 있다.parent
포인터를 따라 부모 클래스로 이동OBject
에 이르러서도 적절한 메서드를 찾지 못한 경우, 에러를 발생시키고 메서드를 종료한다.self
참조를 이용해서 결정한다.module
)trait
)protocol
), 확장(extension
)Lecture lecture = new GradeLecture(...);
lecture.evaluate();
Lecture lecture = new GradeLecture(...);
lecture.average();
Lecture
에서 메서드를 조회한다.average()
와 average*(String gradeName)
은 메서드 이름은 동일하나 메서드 시그니처가 다르다.일부 언어에서는 메서드 오버로딩을 제공하지 않는다.
class Lecture
{
public:
virtual int average();
virtual int average(std::string grade);
virtual int average(std::string grade, int base);
};
class GradeLecture: public Lecture
{
public:
virtual int average(char grade);
};
부모 클래스에서 선언된 메서드를 호출하면 에러가 발생한다.
GradeLecture *lecture = new GradeLecture();
lecture->average('A');
lecture->average(); // 에러!
lecture->average("A"); // 에러!
lecture->average("A", 70); // 에러!
class GradeLecture: public Lecture
{
public:
virtual int average();
virtual int average(str::string grade); // 부모의 메서드
virtual int average(int base); // 부모의 메서드
virtual int average(char grade); // 부모의 메서드
};
또는 부모 클래스에 정의된 average
라는 이름을 자식 클래스의 네임스페이스에 합칠 수도 있다.
class GradeLecture: public Lecture
{
public:
using Lecture::average;
virtual int average(char grade);
};
⚠️ 주의사항
동적 메서드 탐색과 관련된 규칙이 언어마다 다를 수 있다.
사용하는 언어의 문법과 메서드 탐색 규칙을 주의 깊게 살펴보자!
Lecture
(시작) ~ Object
(종료) 상속계층GradeLecture
(시작) ~ Object
(종료) 상속계층self
가 가리키는 객체에 따라 상속 계층의 범위가 동적으로 변한다.self
참조가 동적 문맥을 결정한다는 사실은 종종 어떤 메서드가 실행될지를 예상하기 어렵게 만든다.self
전송(self send): 자신에게 메시지 전송Lecture
에 평가 기준에 대한 정보를 반환하는 stats
메서드를 추가Lecture
의 stats
는 자신의 getEvaluationMethod()
를 호출한다.public class Lecture {
public String stats() {
return String.format("Title: %s, Evaluation Method: %s", title, getEvalulationMehotd());
}
public String getEvaluationMethod() {
return "Pass or Fail";
}
}
getEvaluationMethod()
getEvaluationMethod
메시지를 전송하는 것 ⭕️📢 중요!) 현재 클래스의 메서드를 호출하는 것이 아니라, 현재 객체에게 메시지를 전송하는 것이다.
Lecture
의 인스턴스가 stats
메시지 수신 시, self
참조는 메시지를 수신한 Lecture
인스턴스를 가리키도록 자동으로 할당된다.Lecture
에서 stats
를 발견하고 이를 실행시킨다.getEvaluationMethod
호출 구문을 발견한다.getEvaluationMethod
를 전송한다.Lecture
부터 다시 탐색을 시작하고, Lecture
내부의 getEvaluationMethod
실행 후 탐색을 종료한다.getEvaluationMethod()
의 의미getEvaluatinoMethod
메서드를 실행시켜라getEvaluatinoMethod
메시지를 보내라 ⭕️public class GradeLecture extends Lecture {
@Override
public String getEvaluationMethod() {
return "Grade";
}
}
GradeLecture
인스턴스가 stats
메시지 수신 시, self
참조는 GradeLecture
를 가리키도록 설정된다.GradeLecture
에는 stats
를 처리할 수 있는 코드가 없으므로, 부모 클래스인 Lecture
에서 메서드를 탐색하고, Lecture
의 stats
가 실행된다.stats
실행 중 getEvaluationMethod()
호출 구문을 발견한다.self
가 참조하는 객체에서 시작되는데, 현재 GradeLecture
로 설정되어 있다.GradeLecture
의 getEvaluationMethod
가 실행되고 메서드 탐색이 종료된다.Lecture
클래스의 stats
와 GradeLecture
의 getEvaluationMethod
의 실행 결과가 조합된 문자열이 반환된다.self
전송self
참조가 가리키는 원래 호출 지점으로 복귀시킨다.self
전송 + 깊은 상속 계층 + 메서드 오버라이딩 => 극단적으로 난해한 코드 상속 계층의 정상에서 자신이 메시지를 처리할 수 없다는 사실을 알게 되는 경우, 어떻게 대처해야 할까?
즉, 객체가 메시지를 이해할 수 없다면 어떻게 할까?
이해할 수 없는 메시지를 다루는 방식은, 언어가 정적 타입 언어인지 동적 타입 언어인지에 따라 다르다.
정적 타입 언어인 자바의 경우,
Lecture lecture = new GradeLecture(...);
lecture.unknownMessage(); // 컴파일 에러!
self
참조가 가리키는 현재 객체에게 메시지를 이해할 수 없다는 메시지를 전송한다.self
참조 객체부터 최상위 객체까지 위임된다.이해할 수 없다
라는 메시지에 응답하는 메서드를 구현하는 것이다.이해할 수 없는 메시지와 도메인-특화 언어
- 동적 타입 언어의 특징 = 이해할 수 없는 메시지 처리 가능
- 메타 프로그래밍 영역에서 진가를 발휘함.
- 이 특성으로인해, 도메인-특화 언어(Domain-Specific Language, DSL) 개발에 더 쉽고 강력하다.
super
참조로 인해 부모 클래스에게 evaluate
메시지가 전송된다. (스스로의 것이 아닌)public class GradeLecture extends Lecture {
@Override
public String evaluate() {
return super.evaluate() + ", " + gradeStatistics();
}
}
super
참조로 인해 부모 클래스인 GradeLecture
가 아니라, 그의 부모인 Lecture
의 average()
가 호출된다.public class FormattedGradeLecture extends GradeLecture {
public FormattedGradeLecture(String name, int pass, List<Grade> grades, List<Integer> scores) {
super(name, pass, grades, scores);
}
public String formatAverage() {
return String.format("Avg: %1.1f", super.average());
}
}
지금 이 클래스의 부모 클래스부터 메서드 탐색을 시작하세요
super
를 통해 실행되어야 하는 메서드가 반드시 부모 클래스에 위치하지 않아도 되는 유연성을 제공한다.super
참조를 통한 메시지 전송은 부모 클래스의 인스턴스에게 메시지를 전송하는 것으로 보여, super 전송이라 부른다.GradeLecture
인스턴스 입장에서 self참조 = GradeLecture
인스턴스 자신Lecture
인스턴스 입장에서 self 참조 = GradeLecture
인스턴스GradeLecture에서 Lecture로 self 참조가 공유되는 과정을 루비를 통해 알아보자
stats
의 인자로 this
를 전달받고 있다.this
에는 수신할 객체를 가리키는 self 참조가 보관된다.stats
는 this
에게 getEvaluationMethod
메서드를 전송한다.Lecture
의 getEvaluationMethod
가 아닐 수 있음을 명시적으로 드러낸다.this
에 전달되는 인스턴스가 Lecture
가 아니라 다른 인스턴스가 전달된다면, 해당 객체의 메서드가 실행될 수 있다.class Lecture
def initialize(name, scores)
@name = name
@scores = scores
end
def stats(this) # self 참조 보관
"Name: #{@name}, Evaluation Method: #{this.getEvaluationMethod()}"
end
def getEvaluationMethod()
"Pass or Fail"
end
end
stats
로 전달된 Lecture
의 getEvaluationMethod
가 실행되게 된다.lecture = Lecture.new("OOP", [1,2,3])
puts lecture.stats(lecture)
전통적인 상속관계를 나타내는 구문을 사용하지 않고, 자식 클래스의 인스턴스가 부모 클래스의 인스턴스에 대한 링크를 포함하는 것으로 상속관계를 흉내냈다.
인스턴스 변수인 @parent
에 부모 클래스인 Lecture
의 인스턴스를 할당한다.
class GradeLecture
def initialize(name, canceled, scores)
@parent = Lecture.new(name, scores) # 1. 받은 this를 저장 => 링크 역할
@canceled = canceled
end
def stats(this)
@parent.stats(this) # 2. @parent에게 요청 전달
end
def getEvaluationMethod() # 3. 자신만의 메서드 구현 (메서드 오버라이딩)
"Grade"
end
end
grade_lecture = GradeLecture.new("OOP", false, [1,2,3])
puts grade_lecture.stats(grade_lecture)
GraeLectrue
은 @parent
에 Lecture
의 인스턴스를 생성해서 저장한다.GradeLecutre
에서 Lecutre
인스턴스로 이동할 수 있는 명시적인 링크가 추가된다.GradeLecture
의 stats
는 추가 작업 없이 @parent
에게 요청을 그대로 전달this
를 그대로 전달한다.GradeLecture
의 getEvaluationMethod
는 요청을 @parent
에게 전달하지 않고, 자신만의 방법으로 메서드를 구현한다.GradeLecture
의 stats
는 메시지를 직접 처리하지 않고 Lecture
의 stats
에게 요청을 전달Lecture
의 stats
는 this
에게 getEvaluationMethod
수행을 위임하므로, GradeLecture
의 getEvaluationMethod
가 실행된다.GradeLecture
을 Lecture
의 자식 클래스로 선언하는 것 만으로 이 과정들을 설정해준다.포워딩과 위임
- 포워딩
- 처리를 요청할 때 self 참조를 전달하지 않는 경우
- 요청을 전달받은 최초의 객체에 다시 메시지를 전송할 필요는 없고 단순히 코드를 재사용하고 싶은 경우
- 위임
- self 참조를 전달하는 경우
- 위임의 용도
- 클래스를 이용한 상속 관계를 객체 사이의 합성 관계로 대체해 다형성을 구현하는 것
클래스가 아닌 객체를 이용해서도 상속을 흉내낼 수있다.
클래스 없이 객체만 존재하는 프로토타입 기반의 객체지향 언어
에서 상속을 구현하는 방법은, 객체사이의 위임을 이용하는 것이다.
프로토타입 기반의 객체지향 언어인 자바스크립트를 통해 객체 사이의 상속이 어떻게 이루어지는지 알아보자!
prototype
@parent
와 동일Lecture
의 prototype
이 참조하는 객체에 정의했다.Lecture
를 이용해 생성한 모든 객체들은 prototype
객체에 정의된 메서드를 상속받는다.prototype
에 할당되는 객체는 자바스크립트의 최상위 객체 타입인 Object
다.Lecture
를 이용해 생성되는 모든 객체들은 prototype
이 참조하는 Object
에 정의된 모든 속성과 메서드를 상속받는다.function Lecture(name, scores) {
this.name = name;
this.scores = scores;
}
Lecture.prototype.stats = function() {
return "Name: " + this.name + ", Evaluation Method: " + this.getEvalutationMethod();
}
Lecture.prototype.getEvaluationMethod = function() {
return "Pass or Fail"
}
GradeLecture
의 prototype 에 Lecture
의 인스턴스를 할당했다.GradeLecture
를 이용해 생성된 모든 객체들이 prototype
을 통해 Lecture
에 정의된 모든 속성과 함수에 접근할 수 있다.GradeLecture
의 모든 인스턴스는 Lecture
의 특성을 자동으로 상속받게 된다.prototype
으로 연결된 객체 사이의 경로를 통해 객체 사이의 메서드 탐색이 자동으로 이뤄진다.function GradeLecture(name, canceled, scores) {
Lecture.call(this, name, scores);
this.canceled = canceled;
}
// GradeLecture에 Lecture 인스턴스를 할당했다.
GradeLecture.prototype = new Lecture();
GradeLecture.prototype.constructor = GradeLecture;
GradeLecture.prototype.getEvaluationMethod = function() {
return "Grade"
}
var grade_lecture = new GradeLecture("OOP", false, [1,2,3]);
grade_lecture.stats();
GradeLecture
에 stats
메서드가 존재하는지 검사한다.GradeLecture
에 stats
가 존재하지 않으므로 prototype
을 따라 Lecture
의 인스턴스에 접근한 후 stats
메서드가 존재하는지 살펴본다.Lecture
의 stats
가 실행된다.this.getEvaluationMethod()
문장을 발견한다.prototype
이 참조하는 GradeLecture
의 인스턴스에서 getEvaluationMethod
메서드를 발견하고 이 메서드를 실행해 동적 메서드 탐색이 종료된다.prototype
으로 연결된 객체들의 체인을 거슬러 올라가며 자동으로 메시지에 대한 위임을 처리한다.Lecture
의 stats
의 this
는 Lecture
인스턴스가 아니다. 메시지를 수신한 현재 객체를 가리킨다. 객체지향은 객체를 지향하는 것이다.
클래스는 객체를 편리하게 정의하고 생성하기 위해 제공되는 프로그래밍 구성 요소일 뿐이며, 클래스 없이도 객체 사이의 협력 관계를 구축하는 것이 가능하며 상속 없이도 다형성을 구현하는 것이 가능하다.
객체지향은 다양한 방법으로 도달할 수 있는 하나의 지향점이다.
객체지향을 구현하는 클래스라는 개념에 매몰되지 말고, 메시지와의 협력에 보다 초점을 맞춰야 한다.